Skip to content

Add selection formatting and improved tables - #201

Open
Norkep wants to merge 32 commits into
erictli:mainfrom
Norkep:agent/selection-formatting-table-foundation
Open

Add selection formatting and improved tables#201
Norkep wants to merge 32 commits into
erictli:mainfrom
Norkep:agent/selection-formatting-table-foundation

Conversation

@Norkep

@Norkep Norkep commented Aug 3, 2026

Copy link
Copy Markdown

Summary

  • add a selection-based formatting and link-editing menu with block styles, bold, italic, underline, strikethrough, inline code, text colors, highlights, subscript, and superscript
  • keep keyboard selection native while the editor is focused, and add a persistent accessible selection decoration only after focus leaves the editor
  • preserve the new marks through Markdown parsing, serialization, copy/paste, reloads, source mode, and history-safe content replacement
  • improve /table with persisted column widths and row heights, fit-to-width metadata, row/column controls, resizing, edge auto-scroll, TSV clipboard support, integrity guards, and history-safe transactions
  • pin the TipTap stack to one compatible 3.29.2 version

Scope

This is PR 5 of the Scratch 1.0.1 compatibility series.

It intentionally excludes:

  • image drag and drop
  • editor block drag and drop
  • the TipTap drag-handle extension

Those remain isolated for the following PRs.

Dependencies

This branch is stacked on the earlier compatibility work:

The PR targets main; its diff will shrink as those dependencies merge.

Validation

  • npm test -- --run: 50 test files, 257 tests passed
  • npm run build: TypeScript and Vite production build passed
  • cargo test --manifest-path src-tauri/Cargo.toml --quiet: 89 tests passed
  • cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings: passed
  • npm ls --all: dependency tree valid and TipTap/ProseMirror deduplicated
  • git diff --check: passed

Packaged macOS WKWebView validation:

  • backward selection now behaves a -> ma -> a with Shift+Left / Shift+Right
  • forward selection extends and shrinks without losing its anchor
  • Cmd+Shift+Left and Cmd+Shift+Right select the expected line boundaries
  • the formatting menu appears for a real selection
  • applying bold persists exactly as Markdown without altering table data
  • an inserted table persists with Scratch width/height metadata

Audit note

npm audit reports 5 existing toolchain findings: 2 low and 3 high. The affected versions of Babel, esbuild, picomatch, PostCSS, and Vite are identical to the parent PR4 lockfile; this PR adds no net-new audit finding.

Summary by CodeRabbit

  • New Features

    • Added multi-workspace support with switching, folder management, workspace settings, Preferences, and session restoration.
    • Introduced Notion-style formatting, colors, highlights, enhanced tables, resizing, clipboard support, and table controls.
    • Added note sorting, collapsible folders, configurable editor width, toolbar visibility, and title-bar details.
    • Added draft recovery, conflict resolution, and safer standalone-file editing.
  • Bug Fixes

    • Improved save reliability, external-change handling, preview editing, and protection against data loss.
  • Tests

    • Expanded automated coverage across editing, workspaces, persistence, tables, settings, and window behavior.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change adds workspace-aware routing, revision-safe persistence, draft recovery, scoped settings, window sessions, Preferences support, and a Notion-style Tiptap editor with table editing, Markdown metadata, clipboard handling, and Vitest coverage.

Changes

Workspace, persistence, and editor platform

Layer / File(s) Summary
Test tooling and capabilities
package.json, vitest.config.ts, src-tauri/capabilities/default.json
Adds Vitest scripts, test dependencies, pinned Tiptap extensions, React test configuration, and updated window permissions.
Revision-safe persistence and checkpoints
src-tauri/src/hashing.rs, src-tauri/src/persistence.rs, src-tauri/src/draft_checkpoint.rs, src-tauri/src/note_persistence_tests.rs
Adds SHA-256 revisions, atomic compare-and-swap writes, conflict snapshots, durable draft checkpoints, and filesystem failure-path tests.
Workspace backend routing
src-tauri/src/lib.rs, src-tauri/src/watcher_debounce.rs
Adds workspace sessions, scoped settings, workspace-specific indexes and watchers, routed file events, workspace-aware commands, previews, Git, AI operations, and debounce state.
Window, workspace, and settings UI
src/App.tsx, src/context/*, src/components/layout/*, src/components/notes/*, src/components/settings/*, src/lib/window*, src/lib/workspace*
Adds workspace menus, note sorting, folder-section persistence, Preferences routing, window-session persistence, safe close handling, scoped settings events, and editor appearance controls.
Draft and conflict workflows
src/components/editor/Editor.tsx, src/components/preview/PreviewApp.tsx, src/context/NotesContext.tsx, src/lib/draftCheckpoint.ts, src/lib/conflictResolution.ts
Adds generation-safe saves, dirty-draft checkpoints, revision-based conflict detection, recovery snapshots, conflict resolution, standalone recreation, and persistence-controller wiring.
Markdown and editor document model
src/components/editor/editorHistory.ts, src/components/editor/notion/markdownDocument.ts, src/components/editor/notion/markdownMarks.ts, src/components/editor/notion/selectionDecoration.ts, src/components/editor/notion/NotionMenus.tsx
Adds history-free content replacement, validated color and script marks, selection decoration, table metadata round-tripping, and contextual formatting menus.
Table model and interaction system
src/components/editor/notion/table*.ts, src/components/editor/notion/TableControls.tsx, src/App.css
Adds table normalization, metadata, structural transactions, custom rendering, resizing, drag-and-drop, TSV clipboard operations, auto-scroll, accessibility states, and responsive styling.
Service and type contracts
src/services/*, src/types/note.ts, src/lib/*.ts
Adds workspace, file, note, settings, session, lifecycle, synchronization, serialization, and shortcut contracts with corresponding tests.

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related PRs

  • erictli/scratch#196: Shares substantial workspace, persistence, checkpoint, editor, and settings changes.
  • erictli/scratch#197: Shares workspace, persistence, draft-checkpoint, editor, and window-session functionality.
  • erictli/scratch#200: Shares persistence, preview, lifecycle, settings, editor, and sidebar implementations.

Suggested reviewers: erictli

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Editor
  participant NotesContext
  participant RustWorkspaceSession
  participant Filesystem
  User->>Editor: edit note or switch workspace
  Editor->>NotesContext: flush dirty draft
  NotesContext->>RustWorkspaceSession: save with expected revision
  RustWorkspaceSession->>Filesystem: atomic compare-and-swap write
  Filesystem-->>RustWorkspaceSession: saved revision or conflict
  RustWorkspaceSession-->>NotesContext: typed result
  NotesContext->>RustWorkspaceSession: load workspace or resolve conflict
  RustWorkspaceSession-->>Editor: updated note, recovery, or conflict state
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's primary changes: selection formatting and improved table functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/components/preview/PreviewApp.tsx (1)

103-133: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Guard the load effect against out-of-order resolution.

The effect awaits readFileDirect and getDraftCheckpoint, then calls setters. If filePath changes before the promises settle, the stale result overwrites the new file state, including revisionRef.current. A stale revision then drives a save against the wrong base.

Add a cancellation flag.

🛡️ Proposed fix
   useEffect(() => {
+    let cancelled = false;
     filesService
       .readFileDirect(filePath)
       .then(async (result) => {
         const checkpoint = await draftCheckpointService
           .getDraftCheckpoint(filePath)
           .catch(() => null);
+        if (cancelled) return;
         const recovered =

Return () => { cancelled = true; } from the effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/preview/PreviewApp.tsx` around lines 103 - 133, Update the
useEffect loading flow in PreviewApp to use a cancellation flag initialized per
effect run and return a cleanup that marks it cancelled. After readFileDirect
and getDraftCheckpoint resolve, skip all state updates, revisionRef.current
assignment, conflict handling, and toast calls when cancelled, preventing stale
file loads from overwriting the current file state.

Source: Linters/SAST tools

src-tauri/src/lib.rs (1)

2964-2995: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

create_note still uses a check-then-write sequence that can overwrite a note.

Lines 2968 to 2978 loop until final_id names a path that does not exist. Line 2993 then calls fs::write, which truncates an existing file. Two windows bound to the same workspace can both settle on the same final_id and the second fs::write replaces the first note.

Every other write path in this change moved to compare-and-swap. This command did not. Use the create-only persistence path so a concurrent creation returns a conflict instead of replacing content.

🐛 Proposed fix
-    fs::write(&file_path, &content)
-        .await
-        .map_err(|e| e.to_string())?;
+    let create_path = file_path.clone();
+    let create_content = content.clone();
+    let created = tauri::async_runtime::spawn_blocking(move || {
+        persistence::save_if_revision(&create_path, &create_content, None)
+            .map_err(|error| error.to_string())
+    })
+    .await
+    .map_err(|error| format!("Note create task failed: {error}"))??;
+    if matches!(created, persistence::SaveResult::Conflict { .. }) {
+        return Err("A note with that name was created concurrently".to_string());
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 2964 - 2995, Update create_note’s final
persistence step to use an atomic create-only filesystem operation instead of
fs::write, so concurrent creation of the same final_id returns a conflict rather
than truncating existing content. Preserve the existing uniqueness loop,
parent-directory creation, and content generation while ensuring the write fails
when the target already exists.
src/context/NotesContext.tsx (2)

993-1096: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The noteConflict dependency drops watcher events.

Line 1096 lists noteConflict as a dependency. Every conflict transition tears down the file-change listener and registers a new one. Registration is asynchronous, so any watcher event that arrives in that gap is lost. The listener reads all other mutable state through refs.

noteConflict is read only at Line 1048 to populate syncState.conflict, and reconcileRemoteNote in src/lib/noteSync.ts does not read state.conflict. Hold the conflict in a ref and register the listener once.

🐛 Proposed fix to stabilize the listener
+  const noteConflictRef = useRef<NoteSyncConflict | null>(null);
+  noteConflictRef.current = noteConflict;
-          conflict: noteConflict,
+          conflict: noteConflictRef.current,
-  }, [noteConflict, refreshNotes]);
+  }, [refreshNotes]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/NotesContext.tsx` around lines 993 - 1096, Remove noteConflict
from the useEffect dependency array and store the latest conflict in a ref that
is updated when noteConflict changes. Use that ref when constructing syncState
inside the file-change listener, while keeping refreshNotes as the only
necessary dependency so the listener remains registered without gaps.

488-498: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the side effects out of the setSelectedNoteId updater.

The updater at Line 489 writes selectedNoteIdRef, writes currentNoteRef, and calls setCurrentNote and setNoteConflict. React can invoke an updater more than once, so these effects can repeat or observe inconsistent state. selectedNoteIdRef.current already holds the selected id synchronously, so the branch does not need an updater.

🐛 Proposed fix to keep the updater pure
-        // Only clear selection if we're deleting the currently selected note
-        setSelectedNoteId((prevId) => {
-          if (prevId === id) {
-            selectedNoteIdRef.current = null;
-            currentNoteRef.current = null;
-            setCurrentNote(null);
-            setNoteConflict(null);
-            return null;
-          }
-          return prevId;
-        });
+        // Only clear selection if we're deleting the currently selected note
+        if (selectedNoteIdRef.current === id) {
+          selectedNoteIdRef.current = null;
+          currentNoteRef.current = null;
+          setSelectedNoteId(null);
+          setCurrentNote(null);
+          setNoteConflict(null);
+        }

The same pattern exists at Lines 620-626, 658-669, 692-702, and 733-744. Those ranges are outside this change, so fix them separately if you agree.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/NotesContext.tsx` around lines 488 - 498, In the deletion flow
around the selected-note handling, replace the side-effectful setSelectedNoteId
updater with a direct selectedNoteIdRef.current comparison, then perform the ref
updates and setCurrentNote/setNoteConflict calls outside the updater before
clearing the selected ID. Keep the updater pure, and apply the same pattern to
the corresponding handlers using this pattern elsewhere in NotesContext.

Source: Linters/SAST tools

src/services/notes.ts (1)

102-119: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Delete the placeholder note when the duplicate save conflicts.

duplicateNote calls createNote first, then saveNote. If saveNote returns status: "conflict", the function throws and the empty note created on line 107 stays on disk. The user then sees an error plus a stray untitled note.

🛡️ Proposed fix
   if (result.status === "conflict") {
+    await deleteNote(newNote.id).catch(() => undefined);
     throw new Error("The duplicated note changed before its content was saved");
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/notes.ts` around lines 102 - 119, Update duplicateNote to delete
the newly created placeholder note via the existing note-deletion operation when
saveNote returns a conflict, before throwing the error. Keep the normal
successful return path unchanged and ensure cleanup targets newNote.id.
src/context/ThemeContext.tsx (2)

466-492: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle a failed reset.

resetEditorFontSettings awaits updateGlobalSettings without a try/catch. EditorSettingsSection.tsx line 354 binds it directly to onClick, so the returned promise is never handled. If the write fails, the UI shows reset values, the backend keeps the old values, and the user sees no message. Every other setter in this file wraps the call in try/catch.

🛡️ Proposed fix
-    await updateGlobalSettings({
-        editorFont: defaultEditorFontSettings,
-        ...
-      });
+    try {
+      await updateGlobalSettings({
+        editorFont: defaultEditorFontSettings,
+        textDirection: "auto",
+        editorWidth: "normal",
+        interfaceZoom: 1.0,
+        customEditorWidthPx: null,
+        editorWidthResizeEnabled: null,
+        editorToolbarVisible: null,
+        titleBarModifiedDateVisible: null,
+        titleBarFilenameVisible: null,
+        sidebarWidthPx: null,
+        customColorsLight: null,
+        customColorsDark: null,
+      });
+    } catch (error) {
+      console.error("Failed to reset appearance settings:", error);
+      await loadSettingsFromBackend();
+    }

Add a user-facing message on the failure path as well.

As per coding guidelines: "Implement error handling with user-friendly messages".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 466 - 492, Update
resetEditorFontSettings to wrap updateGlobalSettings in try/catch, preserving
the existing state reset while handling persistence failures. On catch, display
a user-facing error through the file’s established notification/message
mechanism, matching the pattern used by other setters in ThemeContext.

Source: Coding guidelines


714-751: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Memoize the provider value and split state from actions.

The provider passes an inline object literal as the context value. Every ThemeProvider render creates a new object, so every useTheme consumer re-renders. This change adds eight more members to that single value, which widens the blast radius: a title-bar toggle now re-renders each consumer that only reads theme.

Wrap the value in useMemo, or split the context into a data context and an actions context as the project already does for NotesContext.

As per coding guidelines: "Use NotesContext with dual context pattern (data/actions separated) for performance optimization" and "Use useCallback and useMemo for performance-critical paths".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 714 - 751, Memoize the context
value object used by the ThemeContext.Provider with useMemo, including every
referenced state value and action in its dependency list so consumers only
re-render when exposed data or callbacks change. Prefer the existing dual
data/actions context pattern used by NotesContext if ThemeContext already
supports that structure, while preserving the current provider API and all
members listed in the value.

Source: Coding guidelines

🟡 Minor comments (16)
src/App.css-1470-1479 (1)

1470-1479: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the deprecated clip property in the visually hidden announcer.

Stylelint reports clip as deprecated at Line 1476. Use clip-path: inset(50%), which is the current visually hidden pattern and keeps the announcement text available to screen readers.

🎨 Proposed fix
 .notion-table-announcement {
   position: fixed;
   width: 1px;
   height: 1px;
   padding: 0;
   overflow: hidden;
-  clip: rect(0 0 0 0);
+  clip-path: inset(50%);
   white-space: nowrap;
   border: 0;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.css` around lines 1470 - 1479, Update the .notion-table-announcement
visually hidden style by replacing the deprecated clip declaration with
clip-path: inset(50%), while preserving the existing accessibility-focused
hiding behavior and all other declarations.

Source: Linters/SAST tools

src/App.css-1364-1376 (1)

1364-1376: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Lowercase the currentcolor keyword.

Stylelint reports value-keyword-case errors at Line 1368 and Line 1375. CSS-wide keywords must be lowercase for this rule.

🎨 Proposed fix
 .notion-table-edge-resize::before {
   content: "";
   width: 12px;
   height: 4px;
-  border-block: 1px solid currentColor;
+  border-block: 1px solid currentcolor;
 }

 .notion-table-edge-resize.is-column::before {
   width: 4px;
   height: 12px;
   border-block: 0;
-  border-inline: 1px solid currentColor;
+  border-inline: 1px solid currentcolor;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.css` around lines 1364 - 1376, Update the currentColor values in the
::before rules for .notion-table-edge-resize and
.notion-table-edge-resize.is-column to the lowercase currentcolor form required
by Stylelint.

Source: Linters/SAST tools

src/components/editor/notion/tableView.ts-44-54 (1)

44-54: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

restore() leaves the col elements that the preview appended.

Lines 44-46 append col elements when the colgroup holds fewer columns than baselineWidths. restore() restores attributes only, so the appended elements stay in the colgroup after the drag ends. The table then renders extra columns until the next updateScratchTableColumns run. Record the appended elements and remove them in restore().

🐛 Proposed fix
+  const appendedColumns: HTMLTableColElement[] = [];
   while (colgroup.children.length < normalizedBaseline.length) {
-    colgroup.appendChild(colgroup.ownerDocument.createElement("col"));
+    const column = colgroup.ownerDocument.createElement("col");
+    colgroup.appendChild(column);
+    appendedColumns.push(column);
   }
@@
     restore() {
       if (restored) return;
       restored = true;
       restoreAttribute(table, "style", tableStyle);
       restoreAttribute(table, "data-fit-to-width", fitToWidth);
       restoreAttribute(table, "data-column-resizing", resizing);
       columns.forEach((column, index) =>
         restoreAttribute(column, "style", columnStyles[index]),
       );
+      appendedColumns.forEach((column) => column.remove());
     },

Also applies to: 81-91

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/notion/tableView.ts` around lines 44 - 54, Update the
column setup logic around normalizedBaseline and restore() to track any col
elements appended to colgroup, then remove those exact elements during restore()
after restoring the saved attributes. Preserve existing handling for original
columns and ensure no temporary columns remain after resizing ends.
src/components/editor/notion/tableRowResize.ts-29-31 (1)

29-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use toLowerCase() for tag names.

toLocaleLowerCase() follows the host locale. In Turkish and Azeri locales, "I" maps to "ı", so an ancestor such as LI produces the selector segment lı:nth-child(n). The generated rule then matches nothing, and the row-resize preview silently stops working. CSS selectors need ASCII lowercasing.

🐛 Proposed fix
     segments.unshift(
-      `${current.tagName.toLocaleLowerCase()}:nth-child(${siblingIndex})`,
+      `${current.tagName.toLowerCase()}:nth-child(${siblingIndex})`,
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/notion/tableRowResize.ts` around lines 29 - 31, Update
the selector segment construction in the table-row resize logic to call
toLowerCase() instead of toLocaleLowerCase() on current.tagName, ensuring CSS
tag names use locale-independent ASCII lowercasing while preserving the existing
nth-child formatting.
src/components/editor/editorHistory.ts-6-12 (1)

6-12: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Parse loaded HTML in an inert document.

container is created from the live document, so resource-loading attributes such as <img src=... onerror=...> can execute during the innerHTML assignment, before ProseMirror strips unknown nodes. Use an inert document created by document.implementation.createHTMLDocument(). The parse result stays identical, and no script or resource loads run.

🔒 Proposed fix to parse in an inert document
 function normalizeLoadedContent(editor: Editor, content: Content): Content {
   if (typeof content === "string") {
-    const container = document.createElement("div");
+    const inertDocument = document.implementation.createHTMLDocument("");
+    const container = inertDocument.createElement("div");
     container.innerHTML = content;
     const parsed = ProseMirrorDOMParser.fromSchema(editor.schema).parse(container);
     return normalizeNestedTablesInJson(parsed.toJSON());
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/editorHistory.ts` around lines 6 - 12, Update
normalizeLoadedContent to create the HTML parsing container from
document.implementation.createHTMLDocument() instead of the live document, then
assign innerHTML and parse it as before so the normalized ProseMirror result
remains unchanged while scripts and resource loads cannot execute.

Source: Linters/SAST tools

src-tauri/src/lib.rs-3493-3513 (1)

3493-3513: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Silent fallback to default settings hides a routing failure.

Line 3503 maps a failed workspace_for_window to WorkspaceSettings::default(). The window then reports Git as disabled and reports no pinned notes, without any signal that the workspace could not be resolved. A user can interpret that as lost settings.

Log the resolution failure so the cause is visible in diagnostics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 3493 - 3513, Update get_settings to log
when workspace_for_window(window.label()) fails instead of silently converting
the failure to default workspace settings. Preserve the existing default
fallback and merge behavior, but emit a diagnostic containing the window label
and resolution failure before returning the merged settings.
src-tauri/src/lib.rs-3002-3017 (1)

3002-3017: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

create_note does not emit a file-change event.

save_note, delete_note, and move_note all call emit_workspace_file_change so peer windows bound to the same workspace update. create_note returns the new Note without emitting anything.

Two windows on one workspace therefore disagree about the note list until the file watcher fires or the user triggers a relist. Emit a "created" semantic event here for consistency with the new event contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 3002 - 3017, The create_note flow must
notify peer windows after successfully creating and indexing the note. Before
returning the new Note, call emit_workspace_file_change with the workspace,
created note path/identity, and the "created" semantic event, matching the event
contract used by save_note, delete_note, and move_note.
src-tauri/src/lib.rs-209-232 (1)

209-232: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Carry the selected note only when the workspace matches.

fallback_main_session clones the stored main record at line 225 and then overrides only workspace at line 230. selected_note_id from the previous, unavailable workspace stays in the returned session.

The main window then tries to select a note id that belongs to a different workspace. The result is either a missing-note error or an unrelated note with the same relative id.

Clear selected_note_id when the workspace changes.

🐛 Proposed fix
     let mut fallback = config
         .records
         .get("main")
         .cloned()
         .unwrap_or_else(|| WindowSession::for_workspace(workspace.clone()));
+    if fallback.workspace != workspace {
+        fallback.selected_note_id = None;
+    }
     fallback.workspace = workspace;
     Some(fallback)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 209 - 232, Update fallback_main_session so
selected_note_id is preserved only when the cloned main record already belongs
to the selected workspace; otherwise clear it before returning the fallback
session. Keep the existing workspace assignment and default WindowSession
behavior unchanged.
src-tauri/src/lib.rs-3643-3649 (1)

3643-3649: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Hold the settings lock across the save.

Line 3644 takes a write lock, mutates git_enabled, and drops the lock at line 3646. Line 3648 then takes a read lock and saves. Another command can change the same settings between the two blocks, and this call persists the newer in-memory state under the assumption that it wrote its own.

update_workspace_settings at line 3576 keeps one lock across the save. Match that pattern.

🛡️ Proposed fix
-    {
-        let mut settings = workspace.settings().write().expect("settings write lock");
-        settings.git_enabled = enabled;
-    }
-
-    let settings = workspace.settings().read().expect("settings read lock");
-    save_settings(&folder, &settings).map_err(|e| e.to_string())?;
+    let mut settings = workspace.settings().write().expect("settings write lock");
+    settings.git_enabled = enabled;
+    save_settings(&folder, &settings).map_err(|e| e.to_string())?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 3643 - 3649, Update the settings mutation
and persistence flow in the affected command to retain the write lock from the
`git_enabled` assignment through `save_settings`, rather than dropping it and
reacquiring a read lock. Match the lock scope used by
`update_workspace_settings`, ensuring this call saves the state it just
modified.
src/lib/windowShortcutCallsites.test.ts-22-31 (1)

22-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the slice indices so the test cannot pass vacuously.

Line 24 and line 25 use indexOf, which returns -1 when the marker text is absent. If PreferencesApp is renamed, preferencesStart becomes -1, source.slice(-1, preferencesEnd) returns an almost empty string, and the assertions at lines 29 and 30 no longer test the intended code. Line 30 in particular passes for any missing region.

Assert both indices before slicing.

💚 Proposed fix
     const preferencesStart = source.indexOf("function PreferencesApp()");
     const preferencesEnd = source.indexOf("function App()", preferencesStart);
+    expect(preferencesStart).toBeGreaterThanOrEqual(0);
+    expect(preferencesEnd).toBeGreaterThan(preferencesStart);
     const preferencesSource = source.slice(preferencesStart, preferencesEnd);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/windowShortcutCallsites.test.ts` around lines 22 - 31, Guard the
marker lookups in the “keeps Back only for in-window Settings navigation” test
by asserting that both `preferencesStart` and `preferencesEnd` are found before
calling `source.slice`. Keep the existing source-content assertions unchanged so
a renamed or missing `PreferencesApp`/`App` boundary cannot make the test pass
vacuously.
src/App.tsx-182-198 (1)

182-198: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cmd+, closes Preferences instead of keeping it open.

useWindowShortcuts maps the preferences action to toggleSettings. toggleSettings toggles the view. If the user is already in the settings view and presses Cmd+,, the app returns to the notes view. The global keydown handler at Line 286 blocks other shortcuts in the settings view, but this hook is independent, so the toggle still runs. Pass an open-only callback.

Also consider a user-friendly message at Line 187 instead of interpolating the raw error value.

🐛 Proposed fix
+  const openSettings = useCallback(async () => {
+    if (view === "settings") return;
+    try {
+      await flushCurrentDraft();
+    } catch (error) {
+      toast.error("Settings not opened because the draft could not be saved.");
+      console.error("Failed to flush draft before opening settings:", error);
+      return;
+    }
+    setView("settings");
+  }, [flushCurrentDraft, view]);
+
-  useWindowShortcuts({ onOpenPreferences: toggleSettings });
+  useWindowShortcuts({ onOpenPreferences: openSettings });

As per coding guidelines: "Implement error handling with user-friendly messages".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` around lines 182 - 198, Update the Preferences shortcut wiring
in App.tsx so useWindowShortcuts receives an open-only callback rather than
toggleSettings, preventing Cmd+, from closing an already open settings view.
Keep toggleSettings for user-initiated view toggling, and replace the raw error
interpolation in its catch block with a user-friendly message while preserving
the early return.

Source: Coding guidelines

src/lib/windowClose.ts-17-25 (1)

17-25: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Closing continues when recovery returns no path after a save failure.

If flushDraft rejects and persistRecovery resolves with undefined, the function still calls closeWindow. The unsaved content is then lost without any signal to the caller other than saveError. Callers in src/App.tsx (Lines 123-135) only inspect recoveredTo, so no warning appears.

Consider rejecting when a save error has no recovery target, or make the caller report saveError when recoveredTo is absent.

♻️ Proposed change
   } catch (saveError) {
     const recoveredTo = await dependencies.persistRecovery();
+    if (!recoveredTo) throw saveError;
     result = { recoveredTo, saveError };
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/windowClose.ts` around lines 17 - 25, Update the error path in the
window-close flow around flushDraft and persistRecovery so a failed save with no
recoveredTo target does not silently close the window; either reject before
closeWindow or propagate a result that App.tsx reports through saveError when
recoveredTo is absent, while preserving normal recovery and successful-close
behavior.
src/components/settings/SettingsPage.test.tsx-68-72 (1)

68-72: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The drag-region count assertion depends on the host platform.

SettingsPage renders the two data-tauri-drag-region divs only when !isWindows. If isWindows resolves to true in the test environment, this expectation of 2 becomes 0 and the test fails on a Windows machine. Mock or stub the platform module so the assertion is deterministic, or assert the count against the same isWindows value the component uses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/settings/SettingsPage.test.tsx` around lines 68 - 72, Update
the SettingsPage test’s data-tauri-drag-region assertion to use a deterministic
platform value by mocking or stubbing the platform module consumed by
SettingsPage, or derive the expected count from that same isWindows value.
Preserve the existing non-Windows expectation of two regions.
src/context/NotesContext.tsx-327-334 (1)

327-334: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the internal error text with a user-facing message.

Line 333 throws Missing base revision for ${savingNoteId}. Line 391 puts that text into error, which the UI shows. Use a message the user can act on, and keep the identifier in a console log for diagnostics.

♻️ Proposed change
           if (!expectedRevision) {
-            throw new Error(`Missing base revision for ${savingNoteId}`);
+            console.error("Missing base revision for note", savingNoteId);
+            throw new Error(
+              "Could not save this note because its version is unknown. Reload the note and try again.",
+            );
           }

As per coding guidelines: "Implement error handling with user-friendly messages".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/NotesContext.tsx` around lines 327 - 334, Update the save flow in
NotesContext around the expectedRevision validation to throw a user-facing,
actionable message instead of exposing the internal “Missing base revision” text
through the UI error state. Log the detailed missing revision message, including
savingNoteId, via console for diagnostics before throwing the user-friendly
error.

Source: Coding guidelines

src/components/layout/Sidebar.tsx-98-110 (1)

98-110: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not label every switch failure as a cancellation.

The catch block reports "Workspace switch cancelled" for any rejection. switchWorkspace also rejects when the folder is unreadable, missing, or the backend fails. The user then reads a message that contradicts the real cause.

🐛 Proposed fix
       } catch (error) {
         console.error("Failed to switch workspace:", error);
-        toast.error("Workspace switch cancelled");
+        toast.error("Failed to switch folder");
       }

If the backend signals a user cancellation with a distinct error value, branch on that value and keep the cancellation wording only for that case.

As per coding guidelines: "Implement error handling with user-friendly messages".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/Sidebar.tsx` around lines 98 - 110, Update the catch
block in handleSwitchWorkspace to distinguish the backend’s specific
user-cancellation error from other switchWorkspace failures. Keep “Workspace
switch cancelled” only for that cancellation value, and show a user-friendly
failure message for unreadable, missing, or backend-error cases while preserving
error logging.

Source: Coding guidelines

src/lib/useWindowShortcuts.ts-25-28 (1)

25-28: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Report a failed preferences launch to the user.

onOpenPreferences resolves to openPreferencesWindow, which invokes the Rust command open_preferences_window. If that command rejects, void discards the rejection and the user sees no window and no message.

🛡️ Proposed fix
       if (action === "preferences") {
-        void openPreferencesRef.current();
+        void Promise.resolve(openPreferencesRef.current()).catch((error) => {
+          console.error("Failed to open preferences:", error);
+          toast.error("Failed to open Preferences");
+        });
         return;
       }

As per coding guidelines: "Implement error handling with user-friendly messages".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/useWindowShortcuts.ts` around lines 25 - 28, Update the preferences
branch in the window shortcut handler so rejected promises from
openPreferencesRef.current() are caught instead of discarded. Report the failure
to the user with the established user-friendly error notification mechanism,
while preserving the existing successful launch and early-return behavior.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b026ec6-48b3-4335-931c-f84e9a5903c8

📥 Commits

Reviewing files that changed from the base of the PR and between 9126a5a and 7ce1747.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (117)
  • package.json
  • src-tauri/capabilities/default.json
  • src-tauri/src/draft_checkpoint.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/note_persistence_tests.rs
  • src-tauri/src/persistence.rs
  • src-tauri/src/watcher_debounce.rs
  • src/App.css
  • src/App.tsx
  • src/components/editor/Editor.tsx
  • src/components/editor/EditorWidthHandle.test.tsx
  • src/components/editor/EditorWidthHandle.tsx
  • src/components/editor/editorHistory.test.ts
  • src/components/editor/editorHistory.ts
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/components/editor/notion/NotionMenus.tsx
  • src/components/editor/notion/TableControls.test.tsx
  • src/components/editor/notion/TableControls.tsx
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/editor/notion/interfaceGeometry.ts
  • src/components/editor/notion/markdownDocument.test.ts
  • src/components/editor/notion/markdownDocument.ts
  • src/components/editor/notion/markdownMarks.test.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/selectionDecoration.test.ts
  • src/components/editor/notion/selectionDecoration.ts
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/tableClipboard.test.ts
  • src/components/editor/notion/tableClipboard.ts
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/components/editor/notion/tableExtensions.ts
  • src/components/editor/notion/tableIntegrity.test.ts
  • src/components/editor/notion/tableIntegrity.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/components/editor/notion/tableNonRegression.test.ts
  • src/components/editor/notion/tablePerformance.test.ts
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/components/editor/notion/tablePointerDrag.ts
  • src/components/editor/notion/tableProximity.test.ts
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/notion/tableRowResize.test.ts
  • src/components/editor/notion/tableRowResize.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • src/components/editor/notion/tableTransactions.ts
  • src/components/editor/notion/tableView.test.ts
  • src/components/editor/notion/tableView.ts
  • src/components/layout/Sidebar.tsx
  • src/components/layout/SidebarControls.test.tsx
  • src/components/layout/SidebarControls.tsx
  • src/components/layout/SidebarFolderSection.test.tsx
  • src/components/layout/SidebarFolderSection.tsx
  • src/components/layout/WorkspaceMenu.test.tsx
  • src/components/layout/WorkspaceMenu.tsx
  • src/components/notes/FolderTreeView.test.tsx
  • src/components/notes/FolderTreeView.tsx
  • src/components/notes/NoteList.tsx
  • src/components/preview/PreviewApp.tsx
  • src/components/settings/EditorSettingsSection.test.tsx
  • src/components/settings/EditorSettingsSection.tsx
  • src/components/settings/SettingsPage.test.tsx
  • src/components/settings/SettingsPage.tsx
  • src/context/GitContext.tsx
  • src/context/NotesContext.tsx
  • src/context/ThemeContext.tsx
  • src/lib/conflictResolution.test.ts
  • src/lib/conflictResolution.ts
  • src/lib/documentMutationSafety.test.ts
  • src/lib/documentMutationSafety.ts
  • src/lib/draftCheckpoint.test.ts
  • src/lib/draftCheckpoint.ts
  • src/lib/editorToolbar.test.ts
  • src/lib/editorToolbar.ts
  • src/lib/editorWidthResize.test.ts
  • src/lib/editorWidthResize.ts
  • src/lib/folderTree.test.ts
  • src/lib/folderTree.ts
  • src/lib/noteSync.test.ts
  • src/lib/noteSync.ts
  • src/lib/serializedWriter.test.ts
  • src/lib/serializedWriter.ts
  • src/lib/settingsScope.test.ts
  • src/lib/settingsScope.ts
  • src/lib/standaloneRecreation.test.ts
  • src/lib/standaloneRecreation.ts
  • src/lib/standaloneReload.test.ts
  • src/lib/standaloneReload.ts
  • src/lib/titleBarNoteInfo.test.ts
  • src/lib/titleBarNoteInfo.ts
  • src/lib/useWindowSessionPersistence.ts
  • src/lib/useWindowShortcuts.ts
  • src/lib/windowClose.test.ts
  • src/lib/windowClose.ts
  • src/lib/windowCloseCallsites.test.ts
  • src/lib/windowSession.test.ts
  • src/lib/windowSession.ts
  • src/lib/windowShortcutCallsites.test.ts
  • src/lib/windowShortcuts.test.ts
  • src/lib/windowShortcuts.ts
  • src/lib/workspace.test.ts
  • src/lib/workspace.ts
  • src/lib/workspaceSwitch.test.ts
  • src/lib/workspaceSwitch.ts
  • src/services/draftCheckpoint.test.ts
  • src/services/draftCheckpoint.ts
  • src/services/files.test.ts
  • src/services/files.ts
  • src/services/notes.test.ts
  • src/services/notes.ts
  • src/services/windowLifecycle.test.ts
  • src/services/windowLifecycle.ts
  • src/services/windowSession.test.ts
  • src/services/windowSession.ts
  • src/types/note.ts
  • vitest.config.ts

Comment thread src-tauri/capabilities/default.json Outdated
Comment thread src-tauri/src/draft_checkpoint.rs
Comment thread src-tauri/src/lib.rs
Comment thread src-tauri/src/lib.rs
Comment thread src-tauri/src/lib.rs Outdated
Comment thread src/components/editor/notion/tableExtensions.ts
Comment thread src/components/layout/SidebarControls.tsx Outdated
Comment thread src/lib/useWindowShortcuts.ts Outdated
Comment thread src/lib/windowCloseCallsites.test.ts
Comment thread src/services/notes.ts
@Norkep
Norkep force-pushed the agent/selection-formatting-table-foundation branch from 7ce1747 to 516b698 Compare August 4, 2026 05:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/context/ThemeContext.tsx (2)

468-499: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

resetEditorFontSettings has a catch with no try. The file does not parse.

Line 480 starts await updateGlobalSettings({...}) at statement level, and line 494 opens } catch (error) {. No try { block is opened, so the module fails to compile. Biome reports parse errors on lines 494 to 499.

Add the missing try { before the await.

🐛 Proposed fix
     setCustomColorsLightState({});
     setCustomColorsDarkState({});
-    await updateGlobalSettings({
+    try {
+      await updateGlobalSettings({
         editorFont: defaultEditorFontSettings,
         textDirection: "auto",
         editorWidth: "normal",
         interfaceZoom: 1.0,
         customEditorWidthPx: null,
         editorWidthResizeEnabled: null,
         editorToolbarVisible: null,
         titleBarModifiedDateVisible: null,
         titleBarFilenameVisible: null,
         sidebarWidthPx: null,
         customColorsLight: null,
         customColorsDark: null,
       });
     } catch (error) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 468 - 499, Add the missing try
block in resetEditorFontSettings before the await updateGlobalSettings call,
enclosing that operation and its closing logic so the existing catch block is
syntactically paired and the module parses successfully.

Source: Linters/SAST tools


716-721: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Place useMemo before the initialization guard.

When isInitialized is false, ThemeProvider returns null before useMemo. Once initialization completes, React sees an additional hook and throws during the render. Move const contextValue = useMemo<ThemeContextType>(...) above if (!isInitialized) return null;, then return immediately after that guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 716 - 721, Move the contextValue
useMemo hook in ThemeProvider before the isInitialized guard so it executes on
every render. Keep the null return immediately after the guard, preserving the
existing initialized rendering behavior while maintaining consistent hook order.
♻️ Duplicate comments (4)
src-tauri/src/lib.rs (4)

429-444: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

File > Open Folder… still emits the new-window event.

emit_native_open_folder_request emits open-folder-in-new-window, the same event as emit_native_new_window_request. The listener cannot distinguish the two menu actions. Emit a distinct event such as open-folder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 429 - 444, Update
emit_native_open_folder_request to emit a distinct "open-folder" event instead
of "open-folder-in-new-window", while leaving emit_native_new_window_request
unchanged.

446-457: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

workspace_path_key still derives persisted identifiers from DefaultHasher.

get_workspace_search_index_path (Line 1800) writes this key into an on-disk directory name, and workspace_window_label embeds it in persisted records keys. DefaultHasher output is not stable across Rust releases, so a toolchain upgrade orphans every search index and every window-session record. Use a stable digest such as hex_sha256.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 446 - 457, Update workspace_path_key to
derive its persisted identifier with the stable hex_sha256 digest instead of
DefaultHasher, preserving the existing workspace- prefix and path-based input.
Ensure workspace_window_label continues using workspace_path_key so both
search-index directories and session record keys receive stable identifiers.

2073-2075: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Force-destroying editor windows still discards unsaved drafts.

window.destroy() bypasses the close request, so the WebView never flushes its draft and never writes a recovery snapshot. close_window_after_save at Line 2091 exists to sequence that flush. Request a graceful close, or write a recovery snapshot for each window before destroying it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 2073 - 2075, Update the window cleanup
loop to avoid calling window.destroy() directly, which discards unsaved drafts;
request a graceful close that lets each editor flush and write its recovery
snapshot, reusing the existing close_window_after_save flow where applicable.
Ensure every window is processed before force-destruction is considered.

3954-3959: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Import still notifies the hardcoded main window.

Line 3871 resolves the workspace from window.label(), but Lines 3955 to 3959 emit select-note to "main" and focus "main". When a workspace window invokes the import, the wrong window receives the selection. Target window.label().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 3954 - 3959, Update the import
notification and focus flow near the existing metadata selection logic to target
the invoking window’s label instead of the hardcoded "main" label. Use
window.label() for both app.emit_to and the subsequent webview-window lookup,
preserving the existing show and set_focus behavior.
🧹 Nitpick comments (9)
src/components/editor/notion/selectionDecoration.test.ts (2)

61-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve App.css relative to this test file.

process.cwd() depends on where Vitest starts. If the process starts outside the repository root, readFileSync throws at module load and the whole suite fails before any test runs. Resolve the path from import.meta.url instead.

♻️ Proposed fix
-import { readFileSync } from "node:fs";
-import { resolve } from "node:path";
+import { readFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
@@
-const appStyles = readFileSync(
-  resolve(process.cwd(), "src/App.css"),
-  "utf8",
-);
+const appStyles = readFileSync(
+  resolve(dirname(fileURLToPath(import.meta.url)), "../../../App.css"),
+  "utf8",
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/notion/selectionDecoration.test.ts` around lines 61 -
64, Update the appStyles file lookup in selectionDecoration.test.ts to resolve
App.css relative to the test module using import.meta.url, rather than
process.cwd(), while preserving the existing UTF-8 read.

195-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The test names promise extension behavior that the assertions do not check.

In both tests the second dispatch asserts only defaultPrevented === false and then repeats the previous anchor and head. The inline comment states that happy-dom applies no keyboard-selection default, so the "extend" half of each name is not verified. Rename the tests to describe the verified behavior, which is that the extension leaves the event unhandled, or drive the extension with an explicit syncNativeDOMHead call as the first half does.

Also applies to: 214-219, 239-250

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/notion/selectionDecoration.test.ts` at line 195, Update
the affected selection tests around the Shift+ArrowLeft and Shift+ArrowRight
cases so their names match the behavior actually asserted: the extension event
remains unhandled. Alternatively, explicitly invoke syncNativeDOMHead for the
second dispatch and assert the resulting extension. Apply the same correction to
both test cases, not just the first.
src/lib/draftCheckpoint.ts (1)

176-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the Note type import to the top of the file.

reconcileDraftCheckpoint at Line 151 uses Note, but the import appears on the last line. TypeScript hoists the import, so the code compiles. Place the import with the other module-level declarations so the dependency is visible.

♻️ Proposed change
-import type { Note } from "../types/note";

Add at the top of the file:

import type { Note } from "../types/note";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/draftCheckpoint.ts` at line 176, Move the type-only Note import to
the top of the file alongside the other module-level imports, while keeping
reconcileDraftCheckpoint and its Note usage unchanged.
src-tauri/src/lib.rs (1)

1257-1274: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the redundant second session lookup.

For a non-fallback label, Line 1261 already resolved workspace_session(window_label) and returned None. Line 1271 repeats the same lookup and acquires the same two locks again. Return the error directly instead.

♻️ Proposed simplification
         if uses_default_workspace_fallback(window_label) {
             return Ok(WorkspaceRuntime::Main(self));
         }
 
-        self.workspace_session(window_label)
-            .map(WorkspaceRuntime::Session)
-            .ok_or_else(|| format!("Workspace session not found for window: {}", window_label))
+        Err(format!(
+            "Workspace session not found for window: {}",
+            window_label
+        ))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 1257 - 1274, In workspace_for_window,
remove the repeated workspace_session(window_label) lookup in the final error
path; for non-fallback labels, return the existing “Workspace session not found
for window” error directly after the initial lookup returns None, while
preserving the fallback-to-main behavior.
src/lib/noteSync.ts (1)

59-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the three return states.

The function returns three distinct signals: undefined for an unrelated event, null for a deletion, and a string for the current ID. The distinction between null and undefined controls whether the caller clears the open note or ignores the event. Add a doc comment so a later change does not collapse the two.

♻️ Proposed comment
+/**
+ * Resolves the note ID that a file-change event maps the open note to.
+ * Returns `undefined` when the event does not affect `currentId`,
+ * `null` when the note was deleted, and the new ID otherwise.
+ */
 export function resolveRemoteNoteId(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/noteSync.ts` around lines 59 - 71, Add a doc comment directly above
resolveRemoteNoteId documenting its three return states: undefined for unrelated
events, null when the note was deleted, and a string containing the current note
ID otherwise. Explicitly preserve the distinction between null and undefined
because callers use it to clear or ignore the open note.
src/components/editor/Editor.tsx (1)

902-927: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the editor auto-save debounce with the documented 300 ms interval.

The debounce timer is 500 ms. The coding guidelines require 300 ms for auto-save. handleSourceChange already uses 300 ms, so the two paths also disagree.

As per coding guidelines: "Debounce user-triggered operations: auto-save 300ms".

♻️ Proposed change
-    }, 500);
+    }, 300);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/Editor.tsx` around lines 902 - 927, Update the
auto-save debounce timer in the save callback around saveImmediately from 500 ms
to 300 ms, matching handleSourceChange and the documented auto-save interval.
Preserve the existing save validation and error-handling behavior.

Source: Coding guidelines

src/context/NotesContext.tsx (1)

489-498: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the ref writes and nested state updates out of the setSelectedNoteId updater.

React can call a state updater more than once. This updater writes selectedNoteIdRef, writes currentNoteRef, and calls setCurrentNote and setNoteConflict. Keep the updater pure and perform the side effects in the callback body, where selectedNoteIdRef.current already holds the value you need.

♻️ Proposed change
-        setSelectedNoteId((prevId) => {
-          if (prevId === id) {
-            selectedNoteIdRef.current = null;
-            currentNoteRef.current = null;
-            setCurrentNote(null);
-            setNoteConflict(null);
-            return null;
-          }
-          return prevId;
-        });
+        if (selectedNoteIdRef.current === id) {
+          selectedNoteIdRef.current = null;
+          currentNoteRef.current = null;
+          setSelectedNoteId(null);
+          setCurrentNote(null);
+          setNoteConflict(null);
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/NotesContext.tsx` around lines 489 - 498, Update the
selected-note clearing logic around setSelectedNoteId so its updater only
computes and returns the next ID, without ref writes or nested state updates.
Move updates to selectedNoteIdRef, currentNoteRef, setCurrentNote, and
setNoteConflict into the surrounding callback body, using the existing
selectedNoteIdRef.current value to determine when the current selection is
cleared.

Source: Linters/SAST tools

src/components/editor/notion/markdownDocument.ts (1)

576-586: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Skip the second full parse when the document has no tables.

serializeMarkdownDocument always calls manager.parse(markdown) to build visibleDocument. This doubles the parse cost of every save, every clipboard copy, and every source-mode toggle, even for notes that contain no tables. collectTableGeometries only reads visibleDocument inside its table branch.

♻️ Proposed change
 export function serializeMarkdownDocument(
   manager: MarkdownManagerLike,
   document: JSONContent,
 ): string {
   const markdown = manager.serialize(prepareTablesForMarkdown(document));
-  const visibleDocument = manager.parse(markdown);
+  if (collectTables(document).length === 0) return markdown;
+  const visibleDocument = manager.parse(markdown);
   return injectTableMetadata(
     markdown,
     collectTableGeometries(manager, document, visibleDocument),
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/notion/markdownDocument.ts` around lines 576 - 586,
Update serializeMarkdownDocument to detect whether document contains any tables
before calling manager.parse(markdown); skip the parse and geometry collection
when none exist, while preserving the existing visibleDocument and
injectTableMetadata flow for documents with tables. Reuse the existing
table-detection or traversal helpers if available rather than introducing
duplicate detection logic.
src/components/editor/editorHistory.ts (1)

6-12: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

XSS (CWE-79): Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Reachability: Internal

Reachability path
● Entry
  src-tauri/src/lib.rs:2198
  restore_window_session
│
▼
● Hop
  src/components/preview/PreviewApp.tsx
│
▼
● Hop
  src/components/editor/Editor.tsx
│
▼
● Sink
  src/components/editor/editorHistory.ts

Parse string content in an inert HTML document instead of innerHTML.

replaceEditorContentWithoutHistory(editor, currentNote.content) can receive raw note/source content as a string, and string content is parsed with container.innerHTML = content. Use DOMParser.parseFromString for that path to keep resource-loading attributes and inline scripts from exercising the live document environment.

🛡️ Proposed change
 function normalizeLoadedContent(editor: Editor, content: Content): Content {
   if (typeof content === "string" {
-    const container = document.createElement("div");
-    container.innerHTML = content;
-    const parsed = ProseMirrorDOMParser.fromSchema(editor.schema).parse(container);
+    const inert = new DOMParser().parseFromString(
+      `<body>${content}</body>`,
+      "text/html",
+    );
+    const parsed = ProseMirrorDOMParser.fromSchema(editor.schema).parse(
+      inert.body,
+    );
     return normalizeNestedTablesInJson(parsed.toJSON());
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/editorHistory.ts` around lines 6 - 12, Update
normalizeLoadedContent to parse string content with DOMParser.parseFromString
using an inert HTML document instead of assigning content via
container.innerHTML. Pass the resulting document/body into
ProseMirrorDOMParser.fromSchema(editor.schema).parse, preserving
normalizeNestedTablesInJson and the existing non-string handling.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/App.css`:
- Around line 1364-1376: Update both border declarations in
.notion-table-edge-resize::before and
.notion-table-edge-resize.is-column::before to use the lowercase currentcolor
keyword, preserving all other styling unchanged.
- Around line 1470-1479: Update the .notion-table-announcement rule to replace
the deprecated clip declaration with an equivalent clip-path value, preserving
the existing visually hidden behavior and satisfying Stylelint.

In `@src/components/editor/Editor.tsx`:
- Around line 949-955: Update flushAllPendingSaves to await flushSourceSave when
sourceNeedsSaveRef.current is true, then continue to await flushPendingSave
instead of returning early. In toggleSourceMode, when exiting source mode, clear
sourceNeedsSaveRef.current and sourceTimeoutRef so stale source state cannot
persist.

In `@src/components/editor/notion/tableIntegrity.ts`:
- Around line 107-120: Update normalizeNestedTablesInJson to calculate table
width from column occupancy rather than direct cell counts, honoring each cell’s
colspan and active rowspan. Track occupied columns across rows so padding fills
only unoccupied positions and preserves rectangular alignment without creating
extra cells. Keep normalizeCell attrs intact, and add regression cases covering
colspan and overlapping active rowspan scenarios.

In `@src/components/settings/SettingsPage.test.tsx`:
- Around line 70-72: Update the drag-region assertion in the SettingsPage test
to account for the isWindows platform flag, expecting zero elements on Windows
and both guarded elements otherwise; alternatively, mock the platform module to
make the test environment deterministic.

In `@src/context/NotesContext.tsx`:
- Around line 781-788: Update the recovered-draft handling in the NotesContext
provider so the informational message from listDraftCheckpoints is no longer
passed to setError. Route the orphaned.length notification through the existing
toast mechanism or a dedicated informational state, preserving the
singular/plural message and only notifying when recovered drafts are present.

In `@src/lib/draftCheckpoint.ts`:
- Around line 151-175: Update reconcileDraftCheckpoint to compare
checkpoint.metadata.baseRevision with diskNote.revision when deciding whether
recovery is needed, not just checkpoint.markdown against diskNote.content.
Preserve the existing clean-checkpoint result when both the content and base
revision match, and ensure revision mismatches explicitly produce the recovered
draft with diskNote as remote.
- Around line 87-95: When the write in flush fails and pending is restored,
re-arm the existing schedule timer so the checkpoint is retried even if no
further markDirty call occurs. Update the flush catch block to invoke schedule
after assigning pending, while accounting for schedule’s const initialization
order by moving schedule above flush or deferring the call until initialization.

In `@src/lib/useWindowSessionPersistence.ts`:
- Around line 73-78: Update the useEffect persistence flow in
useWindowSessionPersistence so skipHydrationPersistenceRef is consumed
immediately after the isRestored check and before returning for a missing
notesFolder. Preserve the existing skip-and-reset behavior, ensuring later
notesFolder availability cannot discard a real sidebar or focus change.
- Around line 181-184: Update the useCallback close-persistence flow around
geometryCaptureRef.current() so geometry capture failures are isolated and do
not reject the callback or block window closure. Catch or otherwise contain that
failure, then always invoke writer.flush() to persist queued patches, preserving
the existing writer dependency.

---

Outside diff comments:
In `@src/context/ThemeContext.tsx`:
- Around line 468-499: Add the missing try block in resetEditorFontSettings
before the await updateGlobalSettings call, enclosing that operation and its
closing logic so the existing catch block is syntactically paired and the module
parses successfully.
- Around line 716-721: Move the contextValue useMemo hook in ThemeProvider
before the isInitialized guard so it executes on every render. Keep the null
return immediately after the guard, preserving the existing initialized
rendering behavior while maintaining consistent hook order.

---

Duplicate comments:
In `@src-tauri/src/lib.rs`:
- Around line 429-444: Update emit_native_open_folder_request to emit a distinct
"open-folder" event instead of "open-folder-in-new-window", while leaving
emit_native_new_window_request unchanged.
- Around line 446-457: Update workspace_path_key to derive its persisted
identifier with the stable hex_sha256 digest instead of DefaultHasher,
preserving the existing workspace- prefix and path-based input. Ensure
workspace_window_label continues using workspace_path_key so both search-index
directories and session record keys receive stable identifiers.
- Around line 2073-2075: Update the window cleanup loop to avoid calling
window.destroy() directly, which discards unsaved drafts; request a graceful
close that lets each editor flush and write its recovery snapshot, reusing the
existing close_window_after_save flow where applicable. Ensure every window is
processed before force-destruction is considered.
- Around line 3954-3959: Update the import notification and focus flow near the
existing metadata selection logic to target the invoking window’s label instead
of the hardcoded "main" label. Use window.label() for both app.emit_to and the
subsequent webview-window lookup, preserving the existing show and set_focus
behavior.

---

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 1257-1274: In workspace_for_window, remove the repeated
workspace_session(window_label) lookup in the final error path; for non-fallback
labels, return the existing “Workspace session not found for window” error
directly after the initial lookup returns None, while preserving the
fallback-to-main behavior.

In `@src/components/editor/Editor.tsx`:
- Around line 902-927: Update the auto-save debounce timer in the save callback
around saveImmediately from 500 ms to 300 ms, matching handleSourceChange and
the documented auto-save interval. Preserve the existing save validation and
error-handling behavior.

In `@src/components/editor/editorHistory.ts`:
- Around line 6-12: Update normalizeLoadedContent to parse string content with
DOMParser.parseFromString using an inert HTML document instead of assigning
content via container.innerHTML. Pass the resulting document/body into
ProseMirrorDOMParser.fromSchema(editor.schema).parse, preserving
normalizeNestedTablesInJson and the existing non-string handling.

In `@src/components/editor/notion/markdownDocument.ts`:
- Around line 576-586: Update serializeMarkdownDocument to detect whether
document contains any tables before calling manager.parse(markdown); skip the
parse and geometry collection when none exist, while preserving the existing
visibleDocument and injectTableMetadata flow for documents with tables. Reuse
the existing table-detection or traversal helpers if available rather than
introducing duplicate detection logic.

In `@src/components/editor/notion/selectionDecoration.test.ts`:
- Around line 61-64: Update the appStyles file lookup in
selectionDecoration.test.ts to resolve App.css relative to the test module using
import.meta.url, rather than process.cwd(), while preserving the existing UTF-8
read.
- Line 195: Update the affected selection tests around the Shift+ArrowLeft and
Shift+ArrowRight cases so their names match the behavior actually asserted: the
extension event remains unhandled. Alternatively, explicitly invoke
syncNativeDOMHead for the second dispatch and assert the resulting extension.
Apply the same correction to both test cases, not just the first.

In `@src/context/NotesContext.tsx`:
- Around line 489-498: Update the selected-note clearing logic around
setSelectedNoteId so its updater only computes and returns the next ID, without
ref writes or nested state updates. Move updates to selectedNoteIdRef,
currentNoteRef, setCurrentNote, and setNoteConflict into the surrounding
callback body, using the existing selectedNoteIdRef.current value to determine
when the current selection is cleared.

In `@src/lib/draftCheckpoint.ts`:
- Line 176: Move the type-only Note import to the top of the file alongside the
other module-level imports, while keeping reconcileDraftCheckpoint and its Note
usage unchanged.

In `@src/lib/noteSync.ts`:
- Around line 59-71: Add a doc comment directly above resolveRemoteNoteId
documenting its three return states: undefined for unrelated events, null when
the note was deleted, and a string containing the current note ID otherwise.
Explicitly preserve the distinction between null and undefined because callers
use it to clear or ignore the open note.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fd0bffeb-3784-4f7b-9009-97e410a81161

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce1747 and 516b698.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (110)
  • package.json
  • src-tauri/capabilities/default.json
  • src-tauri/src/draft_checkpoint.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/note_persistence_tests.rs
  • src-tauri/src/persistence.rs
  • src-tauri/src/watcher_debounce.rs
  • src/App.css
  • src/App.tsx
  • src/components/editor/Editor.tsx
  • src/components/editor/EditorWidthHandle.test.tsx
  • src/components/editor/EditorWidthHandle.tsx
  • src/components/editor/editorHistory.test.ts
  • src/components/editor/editorHistory.ts
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/components/editor/notion/NotionMenus.tsx
  • src/components/editor/notion/TableControls.test.tsx
  • src/components/editor/notion/TableControls.tsx
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/editor/notion/interfaceGeometry.ts
  • src/components/editor/notion/markdownDocument.test.ts
  • src/components/editor/notion/markdownDocument.ts
  • src/components/editor/notion/markdownMarks.test.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/selectionDecoration.test.ts
  • src/components/editor/notion/selectionDecoration.ts
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/tableClipboard.test.ts
  • src/components/editor/notion/tableClipboard.ts
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/components/editor/notion/tableExtensions.ts
  • src/components/editor/notion/tableIntegrity.test.ts
  • src/components/editor/notion/tableIntegrity.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/components/editor/notion/tableNonRegression.test.ts
  • src/components/editor/notion/tablePerformance.test.ts
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/components/editor/notion/tablePointerDrag.ts
  • src/components/editor/notion/tableProximity.test.ts
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/notion/tableRowResize.test.ts
  • src/components/editor/notion/tableRowResize.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • src/components/editor/notion/tableTransactions.ts
  • src/components/editor/notion/tableView.test.ts
  • src/components/editor/notion/tableView.ts
  • src/components/layout/Sidebar.error.test.ts
  • src/components/layout/Sidebar.tsx
  • src/components/layout/SidebarControls.tsx
  • src/components/layout/WorkspaceMenu.test.tsx
  • src/components/layout/WorkspaceMenu.tsx
  • src/components/notes/NoteList.tsx
  • src/components/preview/PreviewApp.tsx
  • src/components/settings/EditorSettingsSection.test.tsx
  • src/components/settings/EditorSettingsSection.tsx
  • src/components/settings/SettingsPage.test.tsx
  • src/components/settings/SettingsPage.tsx
  • src/context/GitContext.tsx
  • src/context/NotesContext.tsx
  • src/context/ThemeContext.tsx
  • src/lib/conflictResolution.test.ts
  • src/lib/conflictResolution.ts
  • src/lib/documentMutationSafety.test.ts
  • src/lib/documentMutationSafety.ts
  • src/lib/draftCheckpoint.test.ts
  • src/lib/draftCheckpoint.ts
  • src/lib/editorToolbar.test.ts
  • src/lib/editorToolbar.ts
  • src/lib/editorWidthResize.test.ts
  • src/lib/editorWidthResize.ts
  • src/lib/noteSync.test.ts
  • src/lib/noteSync.ts
  • src/lib/serializedWriter.test.ts
  • src/lib/serializedWriter.ts
  • src/lib/settingsScope.test.ts
  • src/lib/settingsScope.ts
  • src/lib/standaloneRecreation.test.ts
  • src/lib/standaloneRecreation.ts
  • src/lib/standaloneReload.test.ts
  • src/lib/standaloneReload.ts
  • src/lib/titleBarNoteInfo.test.ts
  • src/lib/titleBarNoteInfo.ts
  • src/lib/useWindowSessionPersistence.ts
  • src/lib/useWindowShortcuts.ts
  • src/lib/windowClose.test.ts
  • src/lib/windowClose.ts
  • src/lib/windowCloseCallsites.test.ts
  • src/lib/windowSession.test.ts
  • src/lib/windowSession.ts
  • src/lib/windowShortcutCallsites.test.ts
  • src/lib/windowShortcuts.test.ts
  • src/lib/windowShortcuts.ts
  • src/lib/workspace.test.ts
  • src/lib/workspace.ts
  • src/lib/workspaceSwitch.test.ts
  • src/lib/workspaceSwitch.ts
  • src/services/draftCheckpoint.test.ts
  • src/services/draftCheckpoint.ts
  • src/services/files.test.ts
  • src/services/files.ts
  • src/services/notes.test.ts
  • src/services/notes.ts
  • src/services/windowLifecycle.test.ts
  • src/services/windowLifecycle.ts
  • src/services/windowSession.test.ts
  • src/services/windowSession.ts
  • src/types/note.ts
💤 Files with no reviewable changes (1)
  • src/components/layout/SidebarControls.tsx
🚧 Files skipped from review as they are similar to previous changes (92)
  • src/lib/windowCloseCallsites.test.ts
  • src/lib/standaloneReload.test.ts
  • src-tauri/capabilities/default.json
  • src/lib/standaloneReload.ts
  • src/lib/workspace.test.ts
  • src/lib/conflictResolution.ts
  • src/lib/workspaceSwitch.ts
  • src/lib/conflictResolution.test.ts
  • src/lib/standaloneRecreation.test.ts
  • src/services/windowSession.test.ts
  • src/lib/windowShortcuts.test.ts
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/interfaceGeometry.ts
  • src/lib/documentMutationSafety.ts
  • src/services/windowSession.ts
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/services/draftCheckpoint.test.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • package.json
  • src/lib/documentMutationSafety.test.ts
  • src/lib/windowShortcutCallsites.test.ts
  • src/services/files.test.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/components/settings/SettingsPage.tsx
  • src/components/editor/EditorWidthHandle.test.tsx
  • src/lib/windowShortcuts.ts
  • src/lib/standaloneRecreation.ts
  • src/lib/windowClose.ts
  • src/lib/editorWidthResize.test.ts
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/settings/EditorSettingsSection.test.tsx
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/lib/workspaceSwitch.test.ts
  • src/lib/windowClose.test.ts
  • src/lib/windowSession.test.ts
  • src-tauri/src/watcher_debounce.rs
  • src/services/windowLifecycle.ts
  • src/components/editor/editorHistory.test.ts
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/lib/editorWidthResize.ts
  • src/components/editor/notion/tableView.test.ts
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/tableRowResize.ts
  • src/components/editor/notion/selectionDecoration.ts
  • src/components/layout/WorkspaceMenu.tsx
  • src/lib/editorToolbar.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/tablePointerDrag.ts
  • src/lib/settingsScope.ts
  • src/lib/titleBarNoteInfo.ts
  • src/lib/noteSync.test.ts
  • src/services/notes.test.ts
  • src/components/layout/WorkspaceMenu.test.tsx
  • src/context/GitContext.tsx
  • src/components/notes/NoteList.tsx
  • src/components/editor/notion/tableRowResize.test.ts
  • src/services/windowLifecycle.test.ts
  • src/components/editor/notion/tableView.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/lib/serializedWriter.ts
  • src/lib/settingsScope.test.ts
  • src/services/files.ts
  • src/lib/titleBarNoteInfo.test.ts
  • src/lib/workspace.ts
  • src/components/editor/notion/tablePerformance.test.ts
  • src-tauri/src/note_persistence_tests.rs
  • src/components/editor/notion/markdownMarks.test.ts
  • src/lib/serializedWriter.test.ts
  • src/components/editor/notion/tableClipboard.ts
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/types/note.ts
  • src/components/editor/notion/markdownDocument.test.ts
  • src/lib/draftCheckpoint.test.ts
  • src/components/settings/EditorSettingsSection.tsx
  • src/components/editor/notion/tableIntegrity.test.ts
  • src/components/editor/notion/NotionMenus.tsx
  • src/components/editor/notion/TableControls.tsx
  • src/components/editor/notion/TableControls.test.tsx
  • src/services/notes.ts
  • src/lib/windowSession.ts
  • src/components/editor/notion/tableProximity.test.ts
  • src/components/layout/Sidebar.tsx
  • src/components/editor/notion/tableClipboard.test.ts
  • src-tauri/src/draft_checkpoint.rs
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/EditorWidthHandle.tsx
  • src/components/editor/notion/tableTransactions.ts
  • src/lib/editorToolbar.test.ts
  • src/components/editor/notion/tableExtensions.ts
  • src-tauri/src/persistence.rs
  • src/App.tsx
  • src/components/editor/notion/tableNonRegression.test.ts

Comment thread src/App.css
Comment thread src/App.css
Comment on lines +949 to +955
const flushAllPendingSaves = useCallback(async () => {
if (sourceNeedsSaveRef.current) {
await flushSourceSave();
return;
}
await flushPendingSave();
}, [flushPendingSave, flushSourceSave]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Flush both pending saves instead of returning after the source flush.

flushAllPendingSaves returns after flushSourceSave when sourceNeedsSaveRef.current is true. toggleSourceMode does not clear sourceNeedsSaveRef when the user leaves source mode, so the flag can remain true while sourceContentRef holds stale text. If the user then edits the rich editor and the window closes or the workspace switches, this path writes the stale source snapshot and skips flushPendingSave, so the newer editor edits are lost.

Flush both pending saves, source first, then the editor.

🛡️ Proposed fix
   const flushAllPendingSaves = useCallback(async () => {
     if (sourceNeedsSaveRef.current) {
       await flushSourceSave();
-      return;
     }
     await flushPendingSave();
   }, [flushPendingSave, flushSourceSave]);

Also clear sourceNeedsSaveRef.current and sourceTimeoutRef when toggleSourceMode exits source mode, so the stale flag cannot survive the transition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/Editor.tsx` around lines 949 - 955, Update
flushAllPendingSaves to await flushSourceSave when sourceNeedsSaveRef.current is
true, then continue to await flushPendingSave instead of returning early. In
toggleSourceMode, when exiting source mode, clear sourceNeedsSaveRef.current and
sourceTimeoutRef so stale source state cannot persist.

Comment thread src/components/editor/notion/tableIntegrity.ts Outdated
Comment thread src/components/settings/SettingsPage.test.tsx
Comment on lines +781 to +788
} else {
const orphaned = await listDraftCheckpoints().catch(() => []);
if (orphaned.length > 0) {
setError(
`${orphaned.length} recovered draft${orphaned.length === 1 ? " is" : "s are"} available from a previous interrupted session`,
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not report recovered drafts through the error channel.

setError drives the provider's error state, which the UI presents as a failure. Recovered drafts are informational, so the user sees an error for a successful recovery. Use a toast or a dedicated informational field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/NotesContext.tsx` around lines 781 - 788, Update the
recovered-draft handling in the NotesContext provider so the informational
message from listDraftCheckpoints is no longer passed to setError. Route the
orphaned.length notification through the existing toast mechanism or a dedicated
informational state, preserving the singular/plural message and only notifying
when recovered drafts are present.

Comment on lines +87 to +95
return enqueue(async () => {
try {
await storage.write(checkpoint);
} catch (error) {
pending ??= checkpoint;
throw error;
}
});
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A failed checkpoint write is restored but never retried.

flush restores pending when storage.write rejects, which shows retry intent. No new timer is armed after the restore. If the user stops editing after a transient failure, nothing calls flush again, so the checkpoint stays in memory and the draft has no recovery snapshot. schedule runs only from markDirty.

Re-arm the timer when you restore the pending checkpoint.

🐛 Proposed fix
   const flush = (): Promise<void> => {
     cancelTimer();
     const checkpoint = pending;
     if (!checkpoint) return operationTail;
     pending = undefined;
 
     return enqueue(async () => {
       try {
         await storage.write(checkpoint);
       } catch (error) {
-        pending ??= checkpoint;
+        pending ??= checkpoint;
+        if (!disposed) schedule();
         throw error;
       }
     });
   };

schedule is declared after flush, so hoisting applies to the const binding. Move schedule above flush, or wrap the call so it runs only after initialization.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return enqueue(async () => {
try {
await storage.write(checkpoint);
} catch (error) {
pending ??= checkpoint;
throw error;
}
});
};
return enqueue(async () => {
try {
await storage.write(checkpoint);
} catch (error) {
pending ??= checkpoint;
if (!disposed) schedule();
throw error;
}
});
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/draftCheckpoint.ts` around lines 87 - 95, When the write in flush
fails and pending is restored, re-arm the existing schedule timer so the
checkpoint is retried even if no further markDirty call occurs. Update the flush
catch block to invoke schedule after assigning pending, while accounting for
schedule’s const initialization order by moving schedule above flush or
deferring the call until initialization.

Comment on lines +151 to +175
export function reconcileDraftCheckpoint(
diskNote: Note,
checkpoint: DraftCheckpoint,
): {
note: Note;
remote: Note | null;
recovered: boolean;
shouldClear: boolean;
} {
if (checkpoint.markdown === diskNote.content) {
return {
note: diskNote,
remote: null,
recovered: false,
shouldClear: true,
};
}

return {
note: { ...diskNote, content: checkpoint.markdown },
remote: diskNote,
recovered: true,
shouldClear: false,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find callers of reconcileDraftCheckpoint and check baseRevision handling.
rg -n -C10 'reconcileDraftCheckpoint|baseRevision' src --type=ts --type=tsx

Repository: erictli/scratch

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and search callers/baseRevision only in files present in the repo.
wc -l src/lib/draftCheckpoint.ts
echo '--- outline draftCheckpoint.ts ---'
ast-grep outline src/lib/draftCheckpoint.ts || true
echo '--- reconcileDraftCheckpoint and baseRevision references ---'
rg -n -C12 'reconcileDraftCheckpoint|baseRevision' $(git ls-files | grep -E '\.(ts|tsx)$' | tr '\n' ' ')

Repository: erictli/scratch

Length of output: 17529


Handle baseRevision mismatches in checkpoint recovery.

reconcileDraftCheckpoint only checks markdown against diskNote.content, but checkpoints record metadata.baseRevision from the note revision at save time. If the disk file changed after recovery, the caller receives the draft with recovered: true and remote: diskNote; handle this either by checking checkpoint.metadata.baseRevision === diskNote.revision here or making the recovery decision explicit in src/context/NotesContext.tsx.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/draftCheckpoint.ts` around lines 151 - 175, Update
reconcileDraftCheckpoint to compare checkpoint.metadata.baseRevision with
diskNote.revision when deciding whether recovery is needed, not just
checkpoint.markdown against diskNote.content. Preserve the existing
clean-checkpoint result when both the content and base revision match, and
ensure revision mismatches explicitly produce the recovered draft with diskNote
as remote.

Comment on lines +73 to +78
useEffect(() => {
if (!isRestored || !notesFolder) return;
if (skipHydrationPersistenceRef.current) {
skipHydrationPersistenceRef.current = false;
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The hydration skip flag can be consumed after a real change.

Line 74 returns before line 75 consumes skipHydrationPersistenceRef. If notesFolder is still null when hydration completes, the flag stays true. When notesFolder becomes non-null later, the first run consumes the flag and drops that patch, so a sidebar or focus change made in between is never persisted. Consume the flag before the notesFolder guard.

🐛 Proposed fix
   useEffect(() => {
-    if (!isRestored || !notesFolder) return;
-    if (skipHydrationPersistenceRef.current) {
-      skipHydrationPersistenceRef.current = false;
-      return;
-    }
+    if (!isRestored) return;
+    if (skipHydrationPersistenceRef.current) {
+      skipHydrationPersistenceRef.current = false;
+      return;
+    }
+    if (!notesFolder) return;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!isRestored || !notesFolder) return;
if (skipHydrationPersistenceRef.current) {
skipHydrationPersistenceRef.current = false;
return;
}
useEffect(() => {
if (!isRestored) return;
if (skipHydrationPersistenceRef.current) {
skipHydrationPersistenceRef.current = false;
return;
}
if (!notesFolder) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/useWindowSessionPersistence.ts` around lines 73 - 78, Update the
useEffect persistence flow in useWindowSessionPersistence so
skipHydrationPersistenceRef is consumed immediately after the isRestored check
and before returning for a missing notesFolder. Preserve the existing
skip-and-reset behavior, ensuring later notesFolder availability cannot discard
a real sidebar or focus change.

Comment thread src/lib/useWindowSessionPersistence.ts
Norkep added 12 commits August 4, 2026 09:13
Verify request_full_window_closure_for_preview uses window.close() instead of window.destroy(). Verify is_full_editor_window only matches main and workspace-*. Add Rust tests proving only main and workspace-* are targeted, preview windows are ignored, and Preferences is not treated as a full editor window.
Move ContentRevision to a dedicated hashing module and use it from persistence. This stabilizes note identity across workspace switches.
Wire native menu actions to their own events. File > New Window opens a new workspace window. File > Open Folder… targets the focused window.
Stabilize window session persistence with best-effort geometry capture and serialized writers that don't swallow failures silently.
Import completion and preview actions target the invoking window label. Best-effort cleanup for preview draft checkpoints.
@Norkep
Norkep force-pushed the agent/selection-formatting-table-foundation branch from 516b698 to 8fa3d41 Compare August 4, 2026 08:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/context/ThemeContext.tsx (2)

724-795: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Split the provider into data and actions contexts.

contextValue combines all settings data and all setters. Any settings-data update re-renders consumers that only need actions. Use the NotesContext dual-context pattern for separate data and actions values.

As per coding guidelines, “Use NotesContext with dual context pattern (data/actions separated) for performance optimization.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 724 - 795, Split the ThemeContext
provider value around contextValue into separate data and actions contexts,
following the existing NotesContext dual-context pattern. Put theme and settings
state values in the data context, setters and reset/reload/cycle functions in
the actions context, and update consumers/types/provider exports to use the
appropriate context while preserving the existing API behavior.

Source: Coding guidelines


559-590: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Recover state after a settings write fails.

These setters update local state before persistence. If updateGlobalSettings() fails, they only log the error. Show a user-facing error and reload or roll back the affected setting so the UI does not display an unsaved value.

As per coding guidelines, “Implement error handling with user-friendly messages.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 559 - 590, Update
setEditorWidthResizeEnabled, setEditorToolbarVisible, and updateTitleBarNoteInfo
so failed updateGlobalSettings calls both show a user-friendly user-facing error
and restore the affected setting from persisted state, either by reloading
settings or rolling back the local state. Preserve the optimistic update
behavior when persistence succeeds.

Source: Coding guidelines

🧹 Nitpick comments (4)
src/services/windowLifecycle.ts (1)

17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document how requestCurrentWindowClose differs from closeWindowAfterSave.

This module now exports two close entry points with different semantics. requestCurrentWindowClose starts a close request, which the onCloseRequested handler in src/App.tsx intercepts to flush the draft. closeWindowAfterSave performs the final destruction after that approval. A caller that picks the wrong one skips draft flushing.

Add a short doc comment so the ordering is explicit at the call site.

♻️ Proposed comment
+/**
+ * Requests a close for the current window. This fires `onCloseRequested`, so
+ * draft flushing still runs. Call `closeWindowAfterSave` only from inside that
+ * handler, after the draft is persisted.
+ */
 export async function requestCurrentWindowClose(): Promise<void> {
   await getCurrentWindow().close();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/windowLifecycle.ts` around lines 17 - 19, Add a concise doc
comment above requestCurrentWindowClose explaining that it initiates the close
request intercepted by onCloseRequested for draft flushing, while
closeWindowAfterSave performs final destruction after approval; make the
ordering and distinction clear for callers.
src-tauri/src/hashing.rs (2)

233-258: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

These two tests do not exercise the code they describe.

workspace_path_key_deterministic and draft_checkpoint_filename_deterministic reimplement the identity encoding locally and then assert sha256_hex(x) == sha256_hex(x). The real producers are workspace_path_key in src-tauri/src/lib.rs and checkpoint_file_name in src-tauri/src/draft_checkpoint.rs. If either changes, these tests still pass.

Either call the real functions, or remove the tests and keep determinism coverage in the owning modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/hashing.rs` around lines 233 - 258, Replace the
self-referential tests workspace_path_key_deterministic and
draft_checkpoint_filename_deterministic with assertions that call the production
functions workspace_path_key and checkpoint_file_name using identical inputs
twice. Remove the locally duplicated identity encoding and verify the real
outputs are deterministic, retaining the 64-character hash assertion only where
applicable.

101-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a known-answer test for the two-block padding path.

The SHA-256 implementation handles 56-byte final remainders with a 128-byte padding tail. Current tests only cover empty input and short inputs that fit padding in one final block, so this branch is uncovered. Add a known digest for an input such as &[b'a'; 56].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/hashing.rs` around lines 101 - 110, Add a known-answer test for
the two-block padding branch in the SHA-256 implementation, using an input such
as &[b'a'; 56] and asserting its expected digest. Keep the existing empty and
short-input tests unchanged while covering the tail_length = 128 path.
src/components/editor/notion/markdownDocument.ts (1)

168-176: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Replace the dynamic RegExp in isFenceClose with string checks.

The static analysis ReDoS warning is a false positive. fence.character is one of two hard-coded literals and fence.length is a number, so no metacharacters reach the pattern, and the generated pattern is a single bounded repetition that cannot backtrack.

The pattern is still recompiled for every line inside an open fence. A string comparison removes both the recompilation and the warning.

♻️ Proposed refactor
 function isFenceClose(
   line: string,
   fence: { character: string; length: number },
 ): boolean {
-  const pattern = new RegExp(
-    `^\\s*${fence.character === "`" ? "`" : "~"}{${fence.length},}\\s*$`,
-  );
-  return pattern.test(line);
+  const trimmed = line.trim();
+  if (trimmed.length < fence.length) return false;
+  for (const character of trimmed) {
+    if (character !== fence.character) return false;
+  }
+  return true;
 }

Note one behavior difference: the original allows only leading whitespace of any length, and CommonMark allows at most three leading spaces on a closing fence. Neither version enforces that limit, so this refactor does not change fence matching for existing documents.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/notion/markdownDocument.ts` around lines 168 - 176,
Replace the dynamic RegExp construction in isFenceClose with string-based
checks: derive the expected fence character, verify optional leading whitespace
and trailing whitespace, and confirm the non-whitespace fence run has at least
fence.length characters. Preserve the current behavior of allowing any amount of
leading whitespace and requiring only the matching fence character followed by
whitespace.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/persistence.rs`:
- Around line 204-215: Update the fallback write flow around direct.write_all,
direct.flush, and direct.sync_all so any failure removes the destination file at
path before returning the original write error. Defer
sync_parent_directory(parent) until after all write, flush, and sync results
have succeeded, ensuring those underlying errors are reported before any
parent-directory sync failure.

In `@src/App.tsx`:
- Around line 527-531: Update the folder-selection flow around the App
component’s open call to use a service wrapper that invokes a Tauri command
instead of calling the native dialog plugin directly. Define and register the
corresponding folder-selection command in src-tauri/src/lib.rs, have it perform
the existing directory-only, single-selection dialog behavior, and update the
frontend to consume the command result.
- Around line 538-557: The open-folder listener currently lives only in
AppContent, so Preferences and preview windows lack a handler. Move the
useEffect containing listen("open-folder") and its cleanup/error handling from
AppContent into App or a shared shell component mounted by every window mode,
preserving its openFolderInWorkspaceWindow dependency and disposal behavior.

In `@src/components/editor/editorHistory.ts`:
- Around line 36-43: Update the editor state refresh in editorHistory.ts to
clear history while preserving existing non-history plugin state, using
EditorState.reconfigure() or an equivalent merge rather than
EditorState.create() with loadedState.plugins. Ensure selection and
search-highlight plugin state survives note/source-mode reloads, and add or
update tests covering preservation of selection decorations.

In `@src/components/editor/notion/tableIntegrity.ts`:
- Around line 43-50: Update the inline-node check in normalizeCellContent to
include wikilink alongside text and hardBreak, ensuring wikilink nodes are
accumulated in pendingInline and normalized into paragraph content rather than
pushed directly as table-cell children.

In `@src/components/settings/SettingsPage.test.tsx`:
- Around line 30-41: Update the SettingsPage test teardown around
mountSettingsPage so every created root is tracked and unmounted during
afterEach, ensuring window keydown listeners are cleaned up. Save the original
navigator.userAgent before the Windows-specific test and restore it in a finally
block, so cleanup occurs even when rendering or assertions fail.

In `@src/context/NotesContext.tsx`:
- Around line 664-673: Guard relocated-note reload responses against stale
selections before calling setCurrentNote. In src/context/NotesContext.tsx lines
664-673, 696-703, and 734-743, reuse the request-token guard from selectNote or
a shared guarded reload helper so each response updates currentNote only if the
corresponding note selection is still current.
- Around line 498-504: Reset hasExternalChanges when deleteNote clears the
selected note. In deleteFolderAction, clear selectedNoteIdRef, currentNoteRef,
noteConflict, and hasExternalChanges together with the selection state; apply
these changes at src/context/NotesContext.tsx lines 498-504 and 625-632.

In `@src/services/notes.test.ts`:
- Around line 169-186: Update the duplicateNote test around the existing
invokeMock assertion to verify exactly one backend invocation. Add a call-count
assertion after checking the duplicate_note command, while preserving the
existing result assertions.

---

Outside diff comments:
In `@src/context/ThemeContext.tsx`:
- Around line 724-795: Split the ThemeContext provider value around contextValue
into separate data and actions contexts, following the existing NotesContext
dual-context pattern. Put theme and settings state values in the data context,
setters and reset/reload/cycle functions in the actions context, and update
consumers/types/provider exports to use the appropriate context while preserving
the existing API behavior.
- Around line 559-590: Update setEditorWidthResizeEnabled,
setEditorToolbarVisible, and updateTitleBarNoteInfo so failed
updateGlobalSettings calls both show a user-friendly user-facing error and
restore the affected setting from persisted state, either by reloading settings
or rolling back the local state. Preserve the optimistic update behavior when
persistence succeeds.

---

Nitpick comments:
In `@src-tauri/src/hashing.rs`:
- Around line 233-258: Replace the self-referential tests
workspace_path_key_deterministic and draft_checkpoint_filename_deterministic
with assertions that call the production functions workspace_path_key and
checkpoint_file_name using identical inputs twice. Remove the locally duplicated
identity encoding and verify the real outputs are deterministic, retaining the
64-character hash assertion only where applicable.
- Around line 101-110: Add a known-answer test for the two-block padding branch
in the SHA-256 implementation, using an input such as &[b'a'; 56] and asserting
its expected digest. Keep the existing empty and short-input tests unchanged
while covering the tail_length = 128 path.

In `@src/components/editor/notion/markdownDocument.ts`:
- Around line 168-176: Replace the dynamic RegExp construction in isFenceClose
with string-based checks: derive the expected fence character, verify optional
leading whitespace and trailing whitespace, and confirm the non-whitespace fence
run has at least fence.length characters. Preserve the current behavior of
allowing any amount of leading whitespace and requiring only the matching fence
character followed by whitespace.

In `@src/services/windowLifecycle.ts`:
- Around line 17-19: Add a concise doc comment above requestCurrentWindowClose
explaining that it initiates the close request intercepted by onCloseRequested
for draft flushing, while closeWindowAfterSave performs final destruction after
approval; make the ordering and distinction clear for callers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4572fb02-f594-4b3e-90b5-c4edd51f17ad

📥 Commits

Reviewing files that changed from the base of the PR and between 516b698 and 8fa3d41.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (64)
  • package.json
  • src-tauri/capabilities/default.json
  • src-tauri/src/draft_checkpoint.rs
  • src-tauri/src/hashing.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/persistence.rs
  • src/App.css
  • src/App.tsx
  • src/components/editor/Editor.tsx
  • src/components/editor/editorHistory.test.ts
  • src/components/editor/editorHistory.ts
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/components/editor/notion/NotionMenus.tsx
  • src/components/editor/notion/TableControls.test.tsx
  • src/components/editor/notion/TableControls.tsx
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/editor/notion/interfaceGeometry.ts
  • src/components/editor/notion/markdownDocument.test.ts
  • src/components/editor/notion/markdownDocument.ts
  • src/components/editor/notion/markdownMarks.test.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/selectionDecoration.test.ts
  • src/components/editor/notion/selectionDecoration.ts
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/tableClipboard.test.ts
  • src/components/editor/notion/tableClipboard.ts
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/components/editor/notion/tableExtensions.ts
  • src/components/editor/notion/tableIntegrity.test.ts
  • src/components/editor/notion/tableIntegrity.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/components/editor/notion/tableNonRegression.test.ts
  • src/components/editor/notion/tablePerformance.test.ts
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/components/editor/notion/tablePointerDrag.ts
  • src/components/editor/notion/tableProximity.test.ts
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/notion/tableRowResize.test.ts
  • src/components/editor/notion/tableRowResize.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • src/components/editor/notion/tableTransactions.ts
  • src/components/editor/notion/tableView.test.ts
  • src/components/editor/notion/tableView.ts
  • src/components/layout/Sidebar.tsx
  • src/components/notes/NoteList.tsx
  • src/components/preview/PreviewApp.tsx
  • src/components/settings/SettingsPage.test.tsx
  • src/context/NotesContext.test.tsx
  • src/context/NotesContext.tsx
  • src/context/ThemeContext.tsx
  • src/lib/serializedWriter.ts
  • src/lib/useWindowSessionPersistence.ts
  • src/lib/useWindowShortcuts.ts
  • src/lib/windowClose.ts
  • src/lib/windowCloseCallsites.test.ts
  • src/lib/windowSession.test.ts
  • src/lib/windowSession.ts
  • src/lib/windowShortcutCallsites.test.ts
  • src/services/notes.test.ts
  • src/services/notes.ts
  • src/services/windowLifecycle.ts
💤 Files with no reviewable changes (1)
  • src-tauri/capabilities/default.json
🚧 Files skipped from review as they are similar to previous changes (45)
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/components/editor/notion/tableRowResize.test.ts
  • src/lib/windowCloseCallsites.test.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/notion/tableClipboard.test.ts
  • src/lib/windowClose.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/lib/windowShortcutCallsites.test.ts
  • src/components/editor/notion/tablePointerDrag.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • src/components/editor/notion/selectionDecoration.ts
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/tableProximity.test.ts
  • package.json
  • src/components/editor/notion/tableExtensions.ts
  • src/lib/useWindowShortcuts.ts
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/editor/notion/tableView.ts
  • src/components/editor/editorHistory.test.ts
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/components/editor/notion/TableControls.test.tsx
  • src/components/notes/NoteList.tsx
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/tablePerformance.test.ts
  • src/components/editor/notion/tableNonRegression.test.ts
  • src/components/editor/notion/selectionDecoration.test.ts
  • src/components/layout/Sidebar.tsx
  • src/components/editor/notion/markdownMarks.test.ts
  • src/components/editor/notion/tableView.test.ts
  • src/components/editor/notion/interfaceGeometry.ts
  • src/App.css
  • src/components/editor/notion/markdownDocument.test.ts
  • src/lib/windowSession.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/tableClipboard.ts
  • src/components/editor/notion/TableControls.tsx
  • src/components/editor/notion/tableRowResize.ts
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/components/editor/notion/NotionMenus.tsx
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/components/preview/PreviewApp.tsx
  • src/components/editor/Editor.tsx
  • src/components/editor/notion/tableTransactions.ts
  • src-tauri/src/lib.rs

Comment thread src-tauri/src/persistence.rs
Comment thread src/App.tsx
Comment on lines +527 to +531
const selected = await open({
directory: true,
multiple: false,
title: "Open Folder",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Route folder selection through a Tauri command.

Lines 527-531 call the native dialog plugin directly from the frontend. Move folder selection into a command defined in src-tauri/src/lib.rs, then call that command with invoke() through a service wrapper.

As per coding guidelines, “All backend operations must go through Tauri commands defined in src-tauri/src/lib.rs.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` around lines 527 - 531, Update the folder-selection flow around
the App component’s open call to use a service wrapper that invokes a Tauri
command instead of calling the native dialog plugin directly. Define and
register the corresponding folder-selection command in src-tauri/src/lib.rs,
have it perform the existing directory-only, single-selection dialog behavior,
and update the frontend to consume the command result.

Source: Coding guidelines

Comment thread src/App.tsx
Comment on lines +538 to +557
useEffect(() => {
let disposed = false;
let unlisten: (() => void) | undefined;
void listen("open-folder", async () => {
if (disposed) return;
try {
await openFolderInWorkspaceWindow();
} catch (error) {
console.error("Failed to open folder:", error);
if (!disposed) toast.error("Failed to open folder");
}
}).then((cleanup) => {
if (disposed) cleanup();
else unlisten = cleanup;
});
return () => {
disposed = true;
unlisten?.();
};
}, [openFolderInWorkspaceWindow]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Register open-folder in every window mode.

This listener exists only in AppContent. Lines 789-809 render Preferences and preview windows without AppContent, so their native open-folder event has no handler. Move this listener to App or a shared shell mounted by all window modes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` around lines 538 - 557, The open-folder listener currently lives
only in AppContent, so Preferences and preview windows lack a handler. Move the
useEffect containing listen("open-folder") and its cleanup/error handling from
AppContent into App or a shared shell component mounted by every window mode,
preserving its openFolderInWorkspaceWindow dependency and disposal behavior.

Comment thread src/components/editor/editorHistory.ts Outdated
Comment thread src/components/editor/notion/tableIntegrity.ts
Comment on lines +30 to +41
function mountSettingsPage(onBack?: () => void) {
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
act(() =>
root.render(
<TooltipProvider>
<SettingsPage onBack={onBack} />
</TooltipProvider>,
),
);
return { container, root };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 3 'mountSettingsPage|root\.unmount|userAgent|addEventListener\("keydown"' \
  src/components/settings/SettingsPage.test.tsx \
  src/components/settings/SettingsPage.tsx

Repository: erictli/scratch

Length of output: 3984


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- SettingsPage.test.tsx relevant section ---\n'
sed -n '1,115p' src/components/settings/SettingsPage.test.tsx

printf '\n--- SettingsPage.tsx useEffect section ---\n'
sed -n '1,100p' src/components/settings/SettingsPage.tsx

printf '\n--- test framework cleanup hooks in settings tests ---\n'
rg -n 'afterEach|beforeEach|afterAll|beforeAll|test\.extend|expect\(' src/components/settings/SettingsPage.test.tsx

Repository: erictli/scratch

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- SettingsPage.test.tsx relevant section ---'
sed -n '1,115p' src/components/settings/SettingsPage.test.tsx

printf '%s\n' ''
printf '%s\n' '--- SettingsPage.tsx useEffect section ---'
sed -n '1,100p' src/components/settings/SettingsPage.tsx

printf '%s\n' ''
printf '%s\n' '--- test framework cleanup hooks in settings tests ---'
rg -n 'afterEach|beforeEach|afterAll|beforeAll|test\.extend|expect\(' src/components/settings/SettingsPage.test.tsx

Repository: erictli/scratch

Length of output: 7490


Unmount settings roots and restore the user agent in teardown.

SettingsPage registers a window keydown listener in useEffect and runs cleanup in the component unmount path. The first two tests only discard { container, root }, so their keydown listeners stay mounted. The Windows test unmounts only after assertions and restores navigator.userAgent only after assertions; render or assertion failures can leak modified DOM or the Windows userAgent into later tests. Track mounted roots in afterEach and restore the user agent from a saved value in finally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/settings/SettingsPage.test.tsx` around lines 30 - 41, Update
the SettingsPage test teardown around mountSettingsPage so every created root is
tracked and unmounted during afterEach, ensuring window keydown listeners are
cleaned up. Save the original navigator.userAgent before the Windows-specific
test and restore it in a finally block, so cleanup occurs even when rendering or
assertions fail.

Comment on lines +498 to +504
if (selectedNoteIdRef.current === id) {
selectedNoteIdRef.current = null;
currentNoteRef.current = null;
setSelectedNoteId(null);
setCurrentNote(null);
setNoteConflict(null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear note synchronization state when deletion clears selection.

deleteNote leaves hasExternalChanges set. deleteFolderAction leaves both noteConflict and hasExternalChanges set. A previous conflict can remain active with no selected note.

  • src/context/NotesContext.tsx#L498-L504: Reset hasExternalChanges when deleting the selected note.
  • src/context/NotesContext.tsx#L625-L632: Clear the selected-note refs, noteConflict, and hasExternalChanges with the selection state.
Proposed state cleanup
 if (selectedNoteIdRef.current === id) {
   selectedNoteIdRef.current = null;
   currentNoteRef.current = null;
   setSelectedNoteId(null);
   setCurrentNote(null);
   setNoteConflict(null);
+  setHasExternalChanges(false);
 }
 
 if (shouldClearSelection) {
+  selectedNoteIdRef.current = null;
+  currentNoteRef.current = null;
   setCurrentNote(null);
   setSelectedNoteId(null);
+  setNoteConflict(null);
+  setHasExternalChanges(false);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (selectedNoteIdRef.current === id) {
selectedNoteIdRef.current = null;
currentNoteRef.current = null;
setSelectedNoteId(null);
setCurrentNote(null);
setNoteConflict(null);
}
if (selectedNoteIdRef.current === id) {
selectedNoteIdRef.current = null;
currentNoteRef.current = null;
setSelectedNoteId(null);
setCurrentNote(null);
setNoteConflict(null);
setHasExternalChanges(false);
}
Suggested change
if (selectedNoteIdRef.current === id) {
selectedNoteIdRef.current = null;
currentNoteRef.current = null;
setSelectedNoteId(null);
setCurrentNote(null);
setNoteConflict(null);
}
let shouldClearSelection = false;
if (selectedNoteIdRef.current && selectedNoteIdRef.current.startsWith(path + "/")) {
shouldClearSelection = true;
}
if (shouldClearSelection) {
selectedNoteIdRef.current = null;
currentNoteRef.current = null;
setCurrentNote(null);
setSelectedNoteId(null);
setNoteConflict(null);
setHasExternalChanges(false);
}
📍 Affects 1 file
  • src/context/NotesContext.tsx#L498-L504 (this comment)
  • src/context/NotesContext.tsx#L625-L632
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/NotesContext.tsx` around lines 498 - 504, Reset
hasExternalChanges when deleteNote clears the selected note. In
deleteFolderAction, clear selectedNoteIdRef, currentNoteRef, noteConflict, and
hasExternalChanges together with the selection state; apply these changes at
src/context/NotesContext.tsx lines 498-504 and 625-632.

Comment on lines +664 to +673
const selectedId = selectedNoteIdRef.current;
if (selectedId && selectedId.startsWith(oldPrefix)) {
const newId = newPrefix + selectedId.substring(oldPrefix.length);
setSelectedNoteId(newId);
notesService.readNote(newId).then((note) => {
setCurrentNote(note);
}).catch((err) => {
setError(err instanceof Error ? err.message : "Failed to read renamed note");
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard relocated-note reloads against a later selection.

Each path starts notesService.readNote(newId) and later calls setCurrentNote(note) without checking whether the user selected another note. A delayed relocation response can overwrite currentNote after selectedNoteId changes.

Reuse the request-token guard from selectNote, or use one shared guarded reload helper.

  • src/context/NotesContext.tsx#L664-L673: Guard the renamed-note reload before updating currentNote.
  • src/context/NotesContext.tsx#L696-L703: Guard the moved-note reload before updating currentNote.
  • src/context/NotesContext.tsx#L734-L743: Guard the moved-folder note reload before updating currentNote.
📍 Affects 1 file
  • src/context/NotesContext.tsx#L664-L673 (this comment)
  • src/context/NotesContext.tsx#L696-L703
  • src/context/NotesContext.tsx#L734-L743
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/NotesContext.tsx` around lines 664 - 673, Guard relocated-note
reload responses against stale selections before calling setCurrentNote. In
src/context/NotesContext.tsx lines 664-673, 696-703, and 734-743, reuse the
request-token guard from selectNote or a shared guarded reload helper so each
response updates currentNote only if the corresponding note selection is still
current.

Comment on lines +169 to +186
it("duplicates a note with a single atomic backend command", async () => {
invokeMock.mockResolvedValueOnce({
id: "Original-Copy",
title: "Original (Copy)",
content: "# Original (Copy)\n\nContent.\n",
path: "/notes/Original-Copy.md",
modified: 1,
revision: "new-revision",
});

const result = await duplicateNote("Original");

expect(invokeMock).toHaveBeenCalledWith("duplicate_note", {
id: "Original",
});
expect(result.id).toBe("Original-Copy");
expect(result.title).toBe("Original (Copy)");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that duplication makes exactly one backend call.

The test proves that duplicate_note occurs. It does not prove that no additional read or write command occurs. Add expect(invokeMock).toHaveBeenCalledTimes(1) to enforce the stated atomic-operation contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/notes.test.ts` around lines 169 - 186, Update the duplicateNote
test around the existing invokeMock assertion to verify exactly one backend
invocation. Add a call-count assertion after checking the duplicate_note
command, while preserving the existing result assertions.

Norkep added a commit to Norkep/scratch that referenced this pull request Aug 4, 2026
@Norkep
Norkep force-pushed the agent/selection-formatting-table-foundation branch from 8fa3d41 to 1c6bc10 Compare August 4, 2026 09:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/context/ThemeContext.tsx (1)

384-393: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add user-facing error feedback for failed settings persistence.

Most setters in this file update local state immediately, then call updateGlobalSettings. If the call fails, the catch block only logs to console.error. The user sees no indication that the setting did not persist, and local state stays out of sync with the backend until the next full reload.

resetEditorFontSettings (Lines 468-500) already uses the better pattern: catch the error, show toast.error, and call loadSettingsFromBackend() to resync. Apply the same pattern to saveThemeSettings, saveFontSettings, setTextDirection, setEditorWidth, setCustomEditorWidthPx, setEditorWidthResizeEnabled, setEditorToolbarVisible, updateTitleBarNoteInfo, setSidebarWidthPx, setCustomColor, resetCustomColor, and resetAllCustomColors.

As per path instructions, src/**/*.{ts,tsx}: "Implement error handling with user-friendly messages."

Also applies to: 440-450, 503-520, 546-575, 577-593, 613-630, 660-707

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 384 - 393, Settings persistence
failures currently only log errors, leaving users uninformed and local state
unsynchronized. Update saveThemeSettings and the listed
setters—saveFontSettings, setTextDirection, setEditorWidth,
setCustomEditorWidthPx, setEditorWidthResizeEnabled, setEditorToolbarVisible,
updateTitleBarNoteInfo, setSidebarWidthPx, setCustomColor, resetCustomColor, and
resetAllCustomColors—to catch failures, display a user-friendly toast.error
message, and call loadSettingsFromBackend() to resync state, following
resetEditorFontSettings.

Source: Path instructions

🧹 Nitpick comments (1)
src/context/ThemeContext.tsx (1)

536-543: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider debouncing interface zoom persistence.

This useEffect persists interfaceZoom to the backend on every state change. setInterfaceZoom is called during interactive zoom actions (Line 523-534), so rapid changes trigger a backend write for each step.

As per path instructions, src/**/*.{ts,tsx}: "Debounce user-triggered operations: auto-save 300ms, search 150ms, file watcher 500ms, git status 1000ms." Debounce this persistence call to avoid redundant backend writes during interactive zoom.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 536 - 543, Debounce the interface
zoom persistence in the useEffect watching interfaceZoom and isInitialized,
delaying updateGlobalSettings({ interfaceZoom }) by 300ms so rapid interactive
changes produce one backend write. Clear the pending timer on dependency changes
or unmount, while preserving the existing initialization guard and error
logging.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/editor/notion/markdownDocument.ts`:
- Around line 10-11: Update TABLE_DELIMITER_PATTERN to match one-column
delimiter rows such as “| --- |” and “| :--- |” by allowing zero additional
delimiter cells, while still requiring a pipe delimiter through the pattern or
existing line.includes("|") validation used by isTableStart.

---

Outside diff comments:
In `@src/context/ThemeContext.tsx`:
- Around line 384-393: Settings persistence failures currently only log errors,
leaving users uninformed and local state unsynchronized. Update
saveThemeSettings and the listed setters—saveFontSettings, setTextDirection,
setEditorWidth, setCustomEditorWidthPx, setEditorWidthResizeEnabled,
setEditorToolbarVisible, updateTitleBarNoteInfo, setSidebarWidthPx,
setCustomColor, resetCustomColor, and resetAllCustomColors—to catch failures,
display a user-friendly toast.error message, and call loadSettingsFromBackend()
to resync state, following resetEditorFontSettings.

---

Nitpick comments:
In `@src/context/ThemeContext.tsx`:
- Around line 536-543: Debounce the interface zoom persistence in the useEffect
watching interfaceZoom and isInitialized, delaying updateGlobalSettings({
interfaceZoom }) by 300ms so rapid interactive changes produce one backend
write. Clear the pending timer on dependency changes or unmount, while
preserving the existing initialization guard and error logging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f9ae27bf-af85-44b8-b8b0-b9173b395e90

📥 Commits

Reviewing files that changed from the base of the PR and between 8fa3d41 and 1c6bc10.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (51)
  • package.json
  • src-tauri/capabilities/default.json
  • src-tauri/src/draft_checkpoint.rs
  • src-tauri/src/git.rs
  • src-tauri/src/hashing.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/note_persistence_tests.rs
  • src-tauri/src/persistence.rs
  • src/App.css
  • src/App.tsx
  • src/components/editor/Editor.tsx
  • src/components/editor/editorHistory.test.ts
  • src/components/editor/editorHistory.ts
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/components/editor/notion/NotionMenus.tsx
  • src/components/editor/notion/TableControls.test.tsx
  • src/components/editor/notion/TableControls.tsx
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/editor/notion/interfaceGeometry.ts
  • src/components/editor/notion/markdownDocument.test.ts
  • src/components/editor/notion/markdownDocument.ts
  • src/components/editor/notion/markdownMarks.test.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/selectionDecoration.test.ts
  • src/components/editor/notion/selectionDecoration.ts
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/tableClipboard.test.ts
  • src/components/editor/notion/tableClipboard.ts
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/components/editor/notion/tableExtensions.ts
  • src/components/editor/notion/tableIntegrity.test.ts
  • src/components/editor/notion/tableIntegrity.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/components/editor/notion/tableNonRegression.test.ts
  • src/components/editor/notion/tablePerformance.test.ts
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/components/editor/notion/tablePointerDrag.ts
  • src/components/editor/notion/tableProximity.test.ts
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/notion/tableRowResize.test.ts
  • src/components/editor/notion/tableRowResize.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • src/components/editor/notion/tableTransactions.ts
  • src/components/editor/notion/tableView.test.ts
  • src/components/editor/notion/tableView.ts
  • src/context/NotesContext.test.tsx
  • src/context/NotesContext.tsx
  • src/context/ThemeContext.tsx
💤 Files with no reviewable changes (1)
  • src-tauri/capabilities/default.json
🚧 Files skipped from review as they are similar to previous changes (46)
  • src/components/editor/notion/interfaceGeometry.ts
  • src/components/editor/notion/tableRowResize.test.ts
  • src/components/editor/notion/selectionDecoration.test.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/components/editor/notion/TableControls.test.tsx
  • src/components/editor/notion/markdownDocument.test.ts
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/editor/notion/tableClipboard.test.ts
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/components/editor/notion/tableIntegrity.test.ts
  • src/components/editor/notion/tableExtensions.ts
  • src-tauri/src/hashing.rs
  • src/components/editor/notion/tablePerformance.test.ts
  • src/components/editor/notion/tableView.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/notion/tableIntegrity.ts
  • src/components/editor/notion/markdownMarks.test.ts
  • src/components/editor/notion/tableView.test.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/components/editor/notion/selectionDecoration.ts
  • src/context/NotesContext.test.tsx
  • src/components/editor/notion/tablePointerDrag.ts
  • src/App.css
  • src/App.tsx
  • src/components/editor/notion/tableRowResize.ts
  • src-tauri/src/persistence.rs
  • src/components/editor/notion/TableControls.tsx
  • src/components/editor/notion/NotionMenus.tsx
  • src/components/editor/notion/tableTransactions.ts
  • src-tauri/src/note_persistence_tests.rs
  • src-tauri/src/draft_checkpoint.rs
  • src/components/editor/notion/tableClipboard.ts
  • src/context/NotesContext.tsx
  • src/components/editor/Editor.tsx
  • package.json
  • src/components/editor/editorHistory.test.ts
  • src-tauri/src/lib.rs
  • src/components/editor/notion/tableProximity.test.ts
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/tableNonRegression.test.ts

Comment thread src/components/editor/notion/markdownDocument.ts Outdated
@Norkep
Norkep force-pushed the agent/selection-formatting-table-foundation branch from 1c6bc10 to ac3d8a3 Compare August 4, 2026 09:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src-tauri/src/lib.rs (2)

1297-1315: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make workspace binding and session removal atomic.

Both paths determine that a workspace has no bindings, release workspace_bindings, and then remove its shared session. A concurrent window open or rebind can bind that workspace after the check but before removal. The new window then has a binding with no session, and workspace_for_window returns Workspace session not found.

  • src-tauri/src/lib.rs#L1297-L1315: Coordinate old-workspace cleanup with binding creation atomically.
  • src-tauri/src/lib.rs#L1360-L1389: Coordinate last-window cleanup with binding creation atomically.

Use one lifecycle lock or one registry operation for binding changes and conditional session removal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 1297 - 1315, Make workspace binding
changes and conditional session cleanup atomic in src-tauri/src/lib.rs:1297-1315
and src-tauri/src/lib.rs:1360-1389. Coordinate the binding creation/rebind logic
with the no-bindings check and removal from workspace_sessions and
workspace_initializers using a shared lifecycle lock or registry operation, so
concurrent opens or rebinds cannot observe a removed session. Update both
cleanup paths consistently.

4716-4729: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Move the index rebuild out of the Tauri command flow.

rebuild_search_index is synchronous and calls SearchIndex::rebuild_index synchronously, which performs filesystem traversal, std::fs::read_to_string, lock contention, and writer.commit(). This can block command processing; make the command async and run the rebuild as blocking work while keeping the search_index mutex scope as small as possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 4716 - 4729, The synchronous rebuild in
rebuild_search_index blocks the Tauri command flow. Convert the command to async
and execute SearchIndex::rebuild_index in blocking work, cloning or preparing
the required folder and ignored_dirs beforehand; acquire the search_index mutex
only inside that blocking operation and keep the guard scoped through the
rebuild, then propagate its result and errors as before.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src-tauri/src/lib.rs`:
- Around line 1297-1315: Make workspace binding changes and conditional session
cleanup atomic in src-tauri/src/lib.rs:1297-1315 and
src-tauri/src/lib.rs:1360-1389. Coordinate the binding creation/rebind logic
with the no-bindings check and removal from workspace_sessions and
workspace_initializers using a shared lifecycle lock or registry operation, so
concurrent opens or rebinds cannot observe a removed session. Update both
cleanup paths consistently.
- Around line 4716-4729: The synchronous rebuild in rebuild_search_index blocks
the Tauri command flow. Convert the command to async and execute
SearchIndex::rebuild_index in blocking work, cloning or preparing the required
folder and ignored_dirs beforehand; acquire the search_index mutex only inside
that blocking operation and keep the guard scoped through the rebuild, then
propagate its result and errors as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f629796-11f6-40f5-82ef-89fdeb54eb94

📥 Commits

Reviewing files that changed from the base of the PR and between 1c6bc10 and ac3d8a3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (51)
  • package.json
  • src-tauri/capabilities/default.json
  • src-tauri/src/draft_checkpoint.rs
  • src-tauri/src/git.rs
  • src-tauri/src/hashing.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/note_persistence_tests.rs
  • src-tauri/src/persistence.rs
  • src/App.css
  • src/App.tsx
  • src/components/editor/Editor.tsx
  • src/components/editor/editorHistory.test.ts
  • src/components/editor/editorHistory.ts
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/components/editor/notion/NotionMenus.tsx
  • src/components/editor/notion/TableControls.test.tsx
  • src/components/editor/notion/TableControls.tsx
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/editor/notion/interfaceGeometry.ts
  • src/components/editor/notion/markdownDocument.test.ts
  • src/components/editor/notion/markdownDocument.ts
  • src/components/editor/notion/markdownMarks.test.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/selectionDecoration.test.ts
  • src/components/editor/notion/selectionDecoration.ts
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/tableClipboard.test.ts
  • src/components/editor/notion/tableClipboard.ts
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/components/editor/notion/tableExtensions.ts
  • src/components/editor/notion/tableIntegrity.test.ts
  • src/components/editor/notion/tableIntegrity.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/components/editor/notion/tableNonRegression.test.ts
  • src/components/editor/notion/tablePerformance.test.ts
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/components/editor/notion/tablePointerDrag.ts
  • src/components/editor/notion/tableProximity.test.ts
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/notion/tableRowResize.test.ts
  • src/components/editor/notion/tableRowResize.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • src/components/editor/notion/tableTransactions.ts
  • src/components/editor/notion/tableView.test.ts
  • src/components/editor/notion/tableView.ts
  • src/context/NotesContext.test.tsx
  • src/context/NotesContext.tsx
  • src/context/ThemeContext.tsx
💤 Files with no reviewable changes (1)
  • src-tauri/capabilities/default.json
🚧 Files skipped from review as they are similar to previous changes (44)
  • src/components/editor/notion/tableAutoScroll.test.ts
  • src/components/editor/notion/tableExtensions.test.ts
  • src/components/editor/notion/tableRowResize.test.ts
  • src/context/NotesContext.test.tsx
  • src/components/editor/editorHistory.test.ts
  • package.json
  • src/components/editor/notion/tableAutoScroll.ts
  • src/components/editor/notion/interfaceGeometry.ts
  • src/components/editor/notion/tableEdgeDrag.ts
  • src/components/editor/notion/tablePerformance.test.ts
  • src/components/editor/notion/interfaceGeometry.test.ts
  • src/components/editor/notion/tablePointerDrag.ts
  • src/components/editor/notion/tableEdgeDrag.test.ts
  • src/components/editor/notion/selectionDecoration.test.ts
  • src/components/editor/notion/tableProximity.test.ts
  • src/components/editor/notion/tableRowResize.ts
  • src/components/editor/notion/tableClipboard.test.ts
  • src/components/editor/notion/tableView.ts
  • src/components/editor/notion/tableMetadata.ts
  • src/components/editor/notion/markdownMarks.test.ts
  • src/components/editor/notion/tableIntegrity.ts
  • src/components/editor/notion/NotionMenus.tsx
  • src-tauri/src/git.rs
  • src/components/editor/notion/tablePointerDrag.test.ts
  • src/components/editor/notion/tableExtensions.ts
  • src/components/editor/notion/tableProximity.ts
  • src/components/editor/notion/markdownMarks.ts
  • src/components/editor/notion/tableTransactions.test.ts
  • src/components/editor/notion/markdownDocument.test.ts
  • src/components/editor/notion/NotionMenus.test.tsx
  • src/components/editor/notion/TableControls.test.tsx
  • src/components/editor/notion/TableControls.tsx
  • src/App.tsx
  • src-tauri/src/note_persistence_tests.rs
  • src-tauri/src/persistence.rs
  • src/components/editor/notion/tableIntegrity.test.ts
  • src/context/ThemeContext.tsx
  • src/components/editor/notion/tableClipboard.ts
  • src-tauri/src/draft_checkpoint.rs
  • src/App.css
  • src/context/NotesContext.tsx
  • src/components/editor/Editor.tsx
  • src/components/editor/notion/tableTransactions.ts
  • src/components/editor/notion/selectionDecoration.ts

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