Skip to content

Conversation

@dannysmith
Copy link
Owner

@dannysmith dannysmith commented Jan 16, 2026

Summary by CodeRabbit

  • New Features

    • Tag input now clears and surfaces validation errors for duplicates/too-many tags; improved deep-link handling; enhanced calendar drag-and-drop and localized date formatting; new layout size tokens and persistent view/task order.
  • Bug Fixes

    • Kanban card edit behavior corrected.
  • Refactor

    • Unified wikilink handling and modularized vault/deep-link flows (internal).
  • Tests

    • Added tests for wikilink utilities, task-order logic, calendar DnD, and date formatting.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

Standardizes wikilink handling, renames many preference fields to camelCase, splits the monolithic vault service into modular vault packages (keys/queries/mutations/utils/init), adds task-order hook factories and tests, extracts deep-link and calendar DnD logic into hooks, adds tag-input validation callback, and centralizes Rust status serialization.

Changes

Cohort / File(s) Summary
Wikilink utilities & usage
tdn-desktop/src/lib/wikilink.ts, tdn-desktop/src/lib/wikilink.test.ts, tdn-desktop/src/components/tasks/TaskDetailPanel.tsx, tdn-desktop/src/components/views/ProjectView.tsx, tdn-desktop/src/components/views/WeekView.tsx, tdn-desktop/src/lib/commands/task-commands.ts
Add wikilink helpers (extractWikilinkTitle, isWikilink, ensureWikilink, stripWikilink) and tests; replace ad-hoc wikilink parsing/ID extraction with stripWikilink.
Vault service refactor
tdn-desktop/src/services/vault.ts (deleted), tdn-desktop/src/services/vault/* (keys.ts, queries.ts, mutations.ts, utils.ts, init.ts, index.ts)
Remove monolithic vault module and replace it with modular vault packages (keys, queries, mutations, utils, init) and a barrel index.ts.
Preferences & bindings rename
tdn-desktop/src/services/preferences.ts, tdn-desktop/src/lib/bindings.ts, tdn-desktop/src/services/vault.test.ts, tdn-desktop/src-tauri/src/types.rs
Rename preference and DummyVaultPaths fields from snake_case to camelCase (e.g., tasks_dirtasksDir), add preferencesQueryKeys factories, update serde rename behavior in Rust types.
Preference UI updates
tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx, .../VaultPane.tsx, .../GeneralPane.tsx, tdn-desktop/src/hooks/use-command-context.ts
UI and hooks updated to use camelCase preference fields and preferencesQueryKeys; payloads, bindings, and query keys adjusted.
Task-order hooks & tests
tdn-desktop/src/hooks/use-task-order.ts, tdn-desktop/src/hooks/use-task-order.test.ts, tdn-desktop/src/hooks/use-area-order.ts, .../use-inbox-order.ts, .../use-project-order.ts
Add non-keyed/keyed task-order hook factories with comprehensive tests; replace previous area/inbox/project order hooks with factory-backed exports.
Deep-link handling
tdn-desktop/src/lib/deep-link-handler.ts, tdn-desktop/src/hooks/use-deep-link.ts
Introduce centralized deep-link processor (processDeepLink, bringWindowToFront) and make the hook a thin delegating listener.
Calendar DnD refactor & hook
tdn-desktop/src/components/calendar/MonthCalendar.tsx, WeekCalendar.tsx, use-calendar-dnd.ts, use-calendar-dnd.test.ts, MonthDayCell.tsx, DayColumn.tsx
Extract DnD logic into useCalendarDnd with tests; components consume the hook; layout min-heights use CSS variables.
Tag input validation API
tdn-desktop/src/components/ui/tag-input.tsx
Export TagInputValidationError and add onValidationError?: (error) prop; invoked on duplicate/max-tags validation failures.
Rust status serialization
tdn-desktop/src-tauri/src/vault/entities.rs, tdn-desktop/src-tauri/src/vault/writer.rs
Add as_kebab_str() for TaskStatus/ProjectStatus and switch YAML frontmatter serialization to use it.
Types & exports adjustments
tdn-desktop/src/types/data.ts (deleted), tdn-desktop/src/types/index.ts
Remove local UI data types (migrated to tauri-generated bindings) and adjust index exports (add calendar-order, remove data).
Store & API tweaks
tdn-desktop/src/store/view-mode-store.ts, tdn-desktop/src/store/task-creation-store.ts, .../task-creation-store.test.ts
Add persistence to view-mode store; simplify updateActiveListSelection signature (drop index param) and update tests.
Date utils, locales & formatting
tdn-desktop/src/lib/date-utils.ts, tdn-desktop/locales/en.json, tdn-desktop/src/lib/date-utils.test.ts, UI date components
Add i18n-aware date parsing/formatting (formatShortDate, parseLocalDate, formatRelativeDate changes), update locales, and update components/tests to use new helpers.
Misc Rust refactors & safety
tdn-desktop/src-tauri/src/lib.rs, .../commands/recovery.rs, .../vault/scanner.rs, .../vault/manager.rs
Modularize app setup, add recovery retention helper, safer thread-pool handling, and manager path resolution refinements.
Theme variables & layout tokens
tdn-desktop/src/theme-variables.css
Add CSS variables: --kanban-column-width, --kanban-column-min-height, --month-day-min-height, --week-day-min-height.
Kanban component wiring
tdn-desktop/src/components/kanban/KanbanColumn.tsx
Stop forwarding onEditClick from SortableKanbanCard to TaskCard.
Misc UI formatting changes
tdn-desktop/src/components/ui/date-button.tsx, tdn-desktop/src/components/ui/date-picker.tsx
Replace inline date formatting with formatShortDate from date-utils.
Package & dependency
tdn-desktop/package.json
Pin @dannysmith/datepicker to 0.1.4 (remove caret).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Desktop app UI #24: Overlapping vault, wikilink, and deep-link changes — likely touches the same vault APIs and helpers.
  • Desktop app UI #33: Related vault refactor and vaultConfigChanged/reinitialize wiring.

Suggested labels

product/desktop

Poem

🐇 I hop through brackets, strip aliases tight,
CamelCase carrots gleam in the night,
Vaults split to rooms, orders dance in line,
Tags give a twitch — validation's fine,
A rabbit cheers this tidy code flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Desktop App Code Reviews' is vague and generic, failing to convey the actual substantial changes in the pull request (refactoring vault services, migrating to camelCase preferences, adding wikilink utilities, modernizing calendar components, etc.). Replace with a specific, descriptive title that highlights the main change, such as 'Refactor vault service architecture and standardize preference naming' or 'Migrate to modular vault services and camelCase property naming'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tdn-desktop/src/lib/wikilink.ts`:
- Around line 46-49: isWikilink currently returns true for an empty wikilink
"[[]]" whereas extractWikilinkTitle returns null, causing downstream functions
like ensureWikilink and stripWikilink to receive inconsistent inputs; update
isWikilink to only return true when a valid title exists by either calling
extractWikilinkTitle(value) and checking for non-null, or by using the same
validation regex/pattern used by extractWikilinkTitle (e.g., require at least
one non-bracket character between [[ and ]]) so both functions agree on what
constitutes a valid wikilink.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07ec2a5 and 17410f1.

⛔ Files ignored due to path filters (10)
  • tdn-desktop/docs/tasks-done/task-2026-01-12-1-markdown-editor-improvements.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-done/task-2026-01-16-9b-rust-eview.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-done/task-2026-01-16-9c-react-review.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-todo/task-9c-react-review.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-todo/task-9d-clean-code-review.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-todo/task-9d-findings-react-components.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-todo/task-9d-findings-react-core.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-todo/task-9d-findings-rust.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-todo/task-9d-findings-synthesis.md is excluded by !**/*.md
  • tdn-desktop/docs/tasks-todo/task-9d-review-implementation-plan.md is excluded by !**/*.md
📒 Files selected for processing (12)
  • tdn-desktop/src/components/kanban/KanbanColumn.tsx
  • tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx
  • tdn-desktop/src/components/preferences/panes/VaultPane.tsx
  • tdn-desktop/src/components/tasks/TaskDetailPanel.tsx
  • tdn-desktop/src/components/views/ProjectView.tsx
  • tdn-desktop/src/components/views/WeekView.tsx
  • tdn-desktop/src/hooks/use-command-context.ts
  • tdn-desktop/src/lib/commands/task-commands.ts
  • tdn-desktop/src/lib/wikilink.test.ts
  • tdn-desktop/src/lib/wikilink.ts
  • tdn-desktop/src/services/preferences.ts
  • tdn-desktop/src/store/view-mode-store.ts
💤 Files with no reviewable changes (1)
  • tdn-desktop/src/components/kanban/KanbanColumn.tsx
✅ Files skipped from review due to trivial changes (1)
  • tdn-desktop/src/hooks/use-command-context.ts
🧰 Additional context used
📓 Path-based instructions (6)
tdn-desktop/**/*.{tsx,ts}

📄 CodeRabbit inference engine (tdn-desktop/AGENTS.md)

tdn-desktop/**/*.{tsx,ts}: Use selector syntax in Zustand hooks to access state (e.g., useUIStore(state => state.leftSidebarVisible)), never destructure from Zustand hooks
Use getState() in callbacks to access Zustand state instead of destructuring
Use type-safe Tauri commands from @/lib/tauri-bindings (tauri-specta) with Result handling, never use string-based invoke()

Files:

  • tdn-desktop/src/components/preferences/panes/VaultPane.tsx
  • tdn-desktop/src/components/tasks/TaskDetailPanel.tsx
  • tdn-desktop/src/services/preferences.ts
  • tdn-desktop/src/components/views/ProjectView.tsx
  • tdn-desktop/src/lib/wikilink.ts
  • tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx
  • tdn-desktop/src/store/view-mode-store.ts
  • tdn-desktop/src/lib/wikilink.test.ts
  • tdn-desktop/src/lib/commands/task-commands.ts
  • tdn-desktop/src/components/views/WeekView.tsx
tdn-desktop/**/*.tsx

📄 CodeRabbit inference engine (tdn-desktop/AGENTS.md)

tdn-desktop/**/*.tsx: Use useTranslation hook from react-i18next in React components for internationalization
Place all user-facing strings in /locales/*.json translation files
Use React Compiler for automatic memoization handling (no manual useMemo/useCallback needed)

Files:

  • tdn-desktop/src/components/preferences/panes/VaultPane.tsx
  • tdn-desktop/src/components/tasks/TaskDetailPanel.tsx
  • tdn-desktop/src/components/views/ProjectView.tsx
  • tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx
  • tdn-desktop/src/components/views/WeekView.tsx
tdn-desktop/**/*.{tsx,css,scss}

📄 CodeRabbit inference engine (tdn-desktop/AGENTS.md)

Use CSS logical properties (e.g., text-start instead of text-left) for RTL support

Files:

  • tdn-desktop/src/components/preferences/panes/VaultPane.tsx
  • tdn-desktop/src/components/tasks/TaskDetailPanel.tsx
  • tdn-desktop/src/components/views/ProjectView.tsx
  • tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx
  • tdn-desktop/src/components/views/WeekView.tsx
tdn-desktop/**/*.{rs,tsx,ts}

📄 CodeRabbit inference engine (tdn-desktop/AGENTS.md)

Emit events from Rust to React using app.emit("event-name", data) and listen with listen("event-name", handler) in React

Files:

  • tdn-desktop/src/components/preferences/panes/VaultPane.tsx
  • tdn-desktop/src/components/tasks/TaskDetailPanel.tsx
  • tdn-desktop/src/services/preferences.ts
  • tdn-desktop/src/components/views/ProjectView.tsx
  • tdn-desktop/src/lib/wikilink.ts
  • tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx
  • tdn-desktop/src/store/view-mode-store.ts
  • tdn-desktop/src/lib/wikilink.test.ts
  • tdn-desktop/src/lib/commands/task-commands.ts
  • tdn-desktop/src/components/views/WeekView.tsx
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

Where appropriate, task/project/area file handling must conform to tdn-specs/S2-interface-design.md and tdn-specs/S3-data-read-write.md specifications

Files:

  • tdn-desktop/src/components/preferences/panes/VaultPane.tsx
  • tdn-desktop/src/components/tasks/TaskDetailPanel.tsx
  • tdn-desktop/src/services/preferences.ts
  • tdn-desktop/src/components/views/ProjectView.tsx
  • tdn-desktop/src/lib/wikilink.ts
  • tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx
  • tdn-desktop/src/store/view-mode-store.ts
  • tdn-desktop/src/lib/wikilink.test.ts
  • tdn-desktop/src/lib/commands/task-commands.ts
  • tdn-desktop/src/components/views/WeekView.tsx
tdn-desktop/**/*.ts

📄 CodeRabbit inference engine (tdn-desktop/AGENTS.md)

Use i18n.t.bind(i18n) for non-React contexts or call i18n.t() directly for occasional use

Files:

  • tdn-desktop/src/services/preferences.ts
  • tdn-desktop/src/lib/wikilink.ts
  • tdn-desktop/src/store/view-mode-store.ts
  • tdn-desktop/src/lib/wikilink.test.ts
  • tdn-desktop/src/lib/commands/task-commands.ts
🧠 Learnings (6)
📚 Learning: 2025-12-28T12:22:45.546Z
Learnt from: CR
Repo: dannysmith/taskdn PR: 0
File: tdn-cli/CLAUDE.md:0-0
Timestamp: 2025-12-28T12:22:45.546Z
Learning: Applies to tdn-cli/src/**/*.{ts,tsx} : Use path aliases in TypeScript imports: `@/*` maps to `src/*` (e.g., `import { foo } from '@/commands/list'`) and `bindings` maps to `bindings/` (e.g., `import { parseTask } from 'bindings`)

Applied to files:

  • tdn-desktop/src/components/tasks/TaskDetailPanel.tsx
  • tdn-desktop/src/components/views/ProjectView.tsx
  • tdn-desktop/src/lib/wikilink.ts
  • tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx
  • tdn-desktop/src/lib/commands/task-commands.ts
  • tdn-desktop/src/components/views/WeekView.tsx
📚 Learning: 2026-01-16T00:27:24.860Z
Learnt from: CR
Repo: dannysmith/taskdn PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T00:27:24.860Z
Learning: Applies to **/*.{ts,tsx,js} : Where appropriate, task/project/area file handling must conform to tdn-specs/S2-interface-design.md and tdn-specs/S3-data-read-write.md specifications

Applied to files:

  • tdn-desktop/src/components/views/ProjectView.tsx
📚 Learning: 2025-12-28T17:09:11.509Z
Learnt from: CR
Repo: dannysmith/taskdn PR: 0
File: tdn-desktop/AGENTS.md:0-0
Timestamp: 2025-12-28T17:09:11.509Z
Learning: Applies to tdn-desktop/**/*.{tsx,ts} : Use type-safe Tauri commands from `@/lib/tauri-bindings` (tauri-specta) with Result handling, never use string-based invoke()

Applied to files:

  • tdn-desktop/src/components/views/ProjectView.tsx
  • tdn-desktop/src/lib/commands/task-commands.ts
📚 Learning: 2025-12-28T17:09:11.509Z
Learnt from: CR
Repo: dannysmith/taskdn PR: 0
File: tdn-desktop/AGENTS.md:0-0
Timestamp: 2025-12-28T17:09:11.509Z
Learning: Applies to tdn-desktop/**/*.{tsx,ts} : Use selector syntax in Zustand hooks to access state (e.g., `useUIStore(state => state.leftSidebarVisible)`), never destructure from Zustand hooks

Applied to files:

  • tdn-desktop/src/store/view-mode-store.ts
📚 Learning: 2025-12-28T17:09:11.509Z
Learnt from: CR
Repo: dannysmith/taskdn PR: 0
File: tdn-desktop/AGENTS.md:0-0
Timestamp: 2025-12-28T17:09:11.509Z
Learning: State management follows the onion pattern: useState (component) → Zustand (global UI) → TanStack Query (persistent data)

Applied to files:

  • tdn-desktop/src/store/view-mode-store.ts
📚 Learning: 2025-12-28T17:09:11.509Z
Learnt from: CR
Repo: dannysmith/taskdn PR: 0
File: tdn-desktop/AGENTS.md:0-0
Timestamp: 2025-12-28T17:09:11.509Z
Learning: Applies to tdn-desktop/**/*.{tsx,ts} : Use getState() in callbacks to access Zustand state instead of destructuring

Applied to files:

  • tdn-desktop/src/store/view-mode-store.ts
🧬 Code graph analysis (7)
tdn-desktop/src/components/preferences/panes/VaultPane.tsx (1)
tdn-desktop/src/services/preferences.ts (1)
  • preferencesQueryKeys (7-12)
tdn-desktop/src/components/tasks/TaskDetailPanel.tsx (1)
tdn-desktop/src/lib/wikilink.ts (1)
  • stripWikilink (72-74)
tdn-desktop/src/components/views/ProjectView.tsx (1)
tdn-desktop/src/lib/wikilink.ts (1)
  • stripWikilink (72-74)
tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx (1)
tdn-desktop/src/services/preferences.ts (1)
  • preferencesQueryKeys (7-12)
tdn-desktop/src/lib/wikilink.test.ts (1)
tdn-desktop/src/lib/wikilink.ts (4)
  • extractWikilinkTitle (25-41)
  • isWikilink (46-49)
  • ensureWikilink (55-58)
  • stripWikilink (72-74)
tdn-desktop/src/lib/commands/task-commands.ts (1)
tdn-desktop/src/lib/wikilink.ts (1)
  • stripWikilink (72-74)
tdn-desktop/src/components/views/WeekView.tsx (1)
tdn-desktop/src/lib/wikilink.ts (1)
  • stripWikilink (72-74)
🔇 Additional comments (21)
tdn-desktop/src/store/view-mode-store.ts (1)

2-2: Persisted view-mode store wiring looks good.
Please sanity-check that view-mode-storage is the intended key and that persistence/hydration behaves as expected in the Tauri webview environment.

Also applies to: 48-61

tdn-desktop/src/services/preferences.ts (1)

7-11: Centralized preference query keys are a nice cleanup.
Please confirm the new keys align with your react-query invalidation/usage patterns across the app to avoid collisions.

tdn-desktop/src/components/preferences/panes/VaultPane.tsx (1)

13-17: Nice consistency improvement using the shared dev-mode key.
Please double-check any cache invalidations or prefetches use the same key factory.

Also applies to: 32-34

tdn-desktop/src/components/preferences/panes/QuickEntryPane.tsx (1)

10-14: Good move to the shared default-shortcut key.
Please ensure any related cache updates/invalidation use the same key factory.

Also applies to: 26-28

tdn-desktop/src/lib/wikilink.ts (3)

13-41: Clear, readable parsing logic.
The alias/heading handling is straightforward and matches the documented examples.


51-58: ensureWikilink behavior looks consistent.
Nice and minimal helper.


60-74: stripWikilink is a good convenience wrapper.
The fallback to trimmed input is sensible for non-wikilink values.

tdn-desktop/src/lib/commands/task-commands.ts (2)

20-20: LGTM!

Good refactor to use the centralized stripWikilink utility from @/lib/wikilink instead of a local helper, promoting code reuse across the codebase.


207-208: LGTM!

The migration to stripWikilink correctly extracts the title/ID from wikilink format (e.g., [[Project Name]]Project Name) before passing to createTask. This aligns with the shared utility pattern adopted across other components.

tdn-desktop/src/lib/wikilink.test.ts (5)

1-7: LGTM!

Clean imports and good organization of the test file, importing all four utility functions being tested.


9-68: Thorough test coverage for extractWikilinkTitle.

The tests comprehensively cover basic wikilinks, aliases, headings, combinations, non-wikilinks, invalid formats, and whitespace handling. Edge cases like heading-only ([[#Heading]]) and alias-only ([[|Alias]]) are appropriately tested to return null.


70-88: LGTM!

Good coverage for isWikilink including valid formats, non-wikilinks, malformed brackets, and whitespace handling.


90-105: LGTM!

Tests for ensureWikilink properly verify wrapping plain text, preserving existing wikilinks, and trimming whitespace.


107-129: LGTM!

Tests for stripWikilink correctly verify extraction, fallback to original string for non-wikilinks, whitespace trimming, and preservation of invalid wikilinks like [[]] and [[#Heading]].

tdn-desktop/src/components/tasks/TaskDetailPanel.tsx (2)

6-6: LGTM!

Correct import of the shared stripWikilink utility using the proper path alias.


99-109: LGTM!

Good refactor of findProjectByWikilink and findAreaByWikilink to use the centralized stripWikilink utility. The lookup logic correctly searches by both title and id, handling cases where the wikilink content could be either.

tdn-desktop/src/components/views/ProjectView.tsx (2)

9-9: LGTM!

Correct import of the shared stripWikilink utility.


102-109: LGTM!

Good refactor of getAreaName to use the centralized stripWikilink utility. The dependency array correctly includes only areas since stripWikilink is a stable module-level import.

tdn-desktop/src/components/views/WeekView.tsx (3)

5-5: LGTM!

Correct import of the shared stripWikilink utility.


102-119: LGTM!

Good refactor of getProjectName and getAreaName to use stripWikilink. Dependency arrays correctly updated to remove the previously-included local extractTitle since stripWikilink is a stable module-level import.


290-311: LGTM!

Navigation handlers handleKanbanProjectClick and handleKanbanAreaClick correctly migrated to use stripWikilink. Dependency arrays properly updated to [projects, navigate] and [areas, navigate] respectively.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
tdn-desktop/src-tauri/src/types.rs (1)

28-52: Add backward-compatible field aliases for legacy snake_case preferences.

The rename_all = "camelCase" attribute will only deserialize camelCase keys from JSON. If existing preferences were persisted in snake_case format, they will be silently ignored and fields will use defaults, causing user settings to reset on upgrade. Add per-field #[serde(alias = "...")] attributes for legacy names to maintain compatibility:

Backward-compat aliases
 #[derive(Debug, Clone, Serialize, Deserialize, Type)]
 #[serde(default, rename_all = "camelCase")]
 pub struct AppPreferences {
     pub theme: String,
+    #[serde(alias = "quick_pane_shortcut")]
     pub quick_pane_shortcut: Option<String>,
     pub language: Option<String>,
+    #[serde(alias = "tasks_dir")]
     pub tasks_dir: Option<String>,
+    #[serde(alias = "areas_dir")]
     pub areas_dir: Option<String>,
+    #[serde(alias = "projects_dir")]
     pub projects_dir: Option<String>,
     pub ignore: Option<Vec<String>>,
+    #[serde(alias = "show_obsidian_features")]
     pub show_obsidian_features: Option<bool>,
+    #[serde(alias = "permanent_delete_tasks")]
     pub permanent_delete_tasks: Option<bool>,
 }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@tdn-desktop/src/lib/deep-link-handler.ts`:
- Around line 280-307: Wrap the body-only update in the same mutation lifecycle
calls as other mutations: call markMutationStart(...) before invoking
commands.updateTask for the body (the branch that checks options.body and uses
newTask.id and options.body), and call markMutationComplete(...) in a finally
block after the await so the mutation marker is always cleared; keep the
existing success handling that updates queryClient.setQueryData for
vaultQueryKeys.task(newTask.id) and vaultQueryKeys.tasks() unchanged so cache
updates remain the same.
- Around line 200-210: handleOpenView currently casts the incoming view to NavId
without validation, which can route to an invalid state; update handleOpenView
to first validate that view is a known NavId (e.g., compare against the
enum/union of allowed NavId values or use a helper isValidNavId) before calling
useNavigationStore.getState().navigate; if view === 'no-area' keep existing
behavior, if validated call navigate({ type: 'nav', id: view as NavId }),
otherwise handle the unknown case by returning false or navigating to a safe
fallback (e.g., a default route) and ensure the function returns the appropriate
boolean.

In `@tdn-desktop/src/services/vault/init.ts`:
- Around line 136-138: The reinitialization path uses formatVaultError which
only returns a string, causing reinitializeVault to skip the logging/toast
behavior used by initializeVault; update the error handling in reinitializeVault
to call handleVaultError(result.error) (or invoke the same logging/toast helper
used by initializeVault) instead of formatVaultError so failures are logged and
a user-facing toast is shown consistently, then rethrow or propagate the
resulting Error as before.
- Around line 44-47: The call to commands.refreshVault() in the early return
path is not handling promise rejections; mirror the existing error-handling
pattern used for the other refreshVault call by either awaiting
commands.refreshVault() inside a try/catch or appending .catch(...) to log the
error and prevent unhandled promise rejections. Ensure you use the same
logger/error message style as the other refreshVault() invocation so failures
are recorded (e.g., wrap commands.refreshVault() in try { await
commands.refreshVault() } catch (err) { processLogger.error(...) } or
commands.refreshVault().catch(err => processLogger.error(...)) before
returning).

In `@tdn-desktop/src/services/vault/mutations.ts`:
- Around line 227-261: The create-project mutation (useCreateProject) never
calls markMutationStart(), so the file-watcher debounce isn't engaged; add a
call to markMutationStart() at the beginning of the mutationFn in
useCreateProject (before logger.debug/commands.createProject) so the debounce is
engaged for the duration of the mutation, keeping the existing
markMutationComplete() in onSuccess (and consider handling
markMutationComplete() on error if needed).
- Around line 40-71: The useCreateTask mutation is missing an onMutate that
calls markMutationStart(), so add an onMutate handler to useCreateTask which
invokes markMutationStart() before the mutation begins (mirroring
useUpdateTask/useDeleteTask), and ensure it returns any necessary context (or
undefined) so the file watcher is prevented from invalidating caches during the
create operation; keep existing onSuccess behavior that calls
markMutationComplete().

In `@tdn-desktop/src/services/vault/queries.ts`:
- Around line 31-33: Import the i18n instance (e.g. import i18n from 'i18next'
or the project's i18n entry) at the top of
tdn-desktop/src/services/vault/queries.ts and localize the operation labels
passed into handleVaultError by replacing raw strings like 'Loading tasks',
'Loading task', 'Loading projects', 'Loading project', 'Loading areas', and
'Loading area' with i18n.t('...') calls; update each throw new
Error(handleVaultError(..., '...')) invocation (the calls around result.status
checks that call handleVaultError) to pass i18n.t('Loading tasks') etc. so the
user-facing messages are translated.
🧹 Nitpick comments (2)
tdn-desktop/src/services/vault/init.ts (1)

5-9: Shadowing module-level queryClient import with hook result.

Line 31 declares const queryClient = useQueryClient() which shadows the module-level import at line 9. While the hook version is correct for React context, this shadowing could cause confusion and potential bugs if someone adds code that inadvertently references the wrong one.

Consider renaming the module-level import or the hook variable for clarity.

💡 Suggested fix
 import { useQueryClient } from '@tanstack/react-query'
 import { listen } from '@tauri-apps/api/event'
 import { useEffect } from 'react'
 import { logger } from '@/lib/logger'
-import { queryClient } from '@/lib/query-client'
+import { queryClient as globalQueryClient } from '@/lib/query-client'
 import { commands, type AppPreferences } from '@/lib/tauri-bindings'

And update line 126:

-  queryClient.clear()
+  globalQueryClient.clear()

Also applies to: 31-31

tdn-desktop/src/hooks/use-task-order.ts (1)

104-130: Consider optimizing the Zustand selector for keyed access.

The current implementation subscribes to the entire orderMap, causing re-renders when any key's order changes, not just the relevant one. For better performance, consider using a selector that extracts only the needed key's order:

💡 Suggested optimization
 export function createKeyedTaskOrderHook(config: KeyedTaskOrderConfig) {
   return function useKeyedTaskOrder(key: string, tasks: Task[]) {
-    // Get order map from Zustand (using selector syntax for performance)
-    const orderMap = useDisplayOrderStore(config.getOrderMap)
-    const storedOrder = orderMap?.[key] ?? null
+    // Get order for specific key from Zustand (using selector with shallow equality)
+    const storedOrder = useDisplayOrderStore(
+      state => config.getOrderMap(state)?.[key] ?? null
+    )

Note: This may require useShallow or a custom equality function if the array reference changes but content is the same.

…implify file age checks, and update documentation for clarity.
…ith error logging and return an empty vector on failure. Update documentation for ThreadPool error handling status.
…sition, path extraction, and ensure lock is released before file I/O. Update documentation to reflect completion status of related tasks.
…emoving unused parameter and updating related tests. Review and document the location of VaultDirs struct for clarity and consistency.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (3)
tdn-desktop/src-tauri/src/commands/recovery.rs (1)

138-176: Don’t drop directory-entry errors silently.
entries.flatten() discards per-entry errors, which can hide filesystem issues and skip files without visibility. Consider logging and continuing.

🐛 Suggested fix
-    for entry in entries.flatten() {
-        let path = entry.path();
+    for entry in entries {
+        let entry = match entry {
+            Ok(entry) => entry,
+            Err(e) => {
+                log::warn!("Failed to read recovery directory entry: {e}");
+                continue;
+            }
+        };
+        let path = entry.path();
tdn-desktop/src/lib/bindings.ts (1)

373-410: Add #[serde(alias)] for each field or implement migration logic to support legacy snake_case preferences.

The Rust structs AppPreferences and DummyVaultPaths use #[serde(rename_all = "camelCase")] but lack field-level #[serde(alias)] attributes or migration logic. If existing on-disk preference files use snake_case keys, deserialization will fail silently or skip fields. The #[serde(default)] attribute only provides defaults for missing fields, not key name conversion. Before release, either:

  1. Add per-field aliases: #[serde(alias = "quick_pane_shortcut")] for each field, or
  2. Implement a migration step in load_preferences_sync() to convert legacy format
tdn-desktop/src/lib/date-utils.ts (1)

109-138: The timezone bug is real: date-only strings are parsed as UTC but extracted with local timezone methods.

When new Date('2025-06-15') is parsed, it creates UTC midnight. Extracting components with getDate(), getMonth(), getFullYear() (which use local timezone) then reconstructing with new Date(year, month, date) causes a mismatch in non-UTC timezones.

For example, in UTC-5:

  • '2025-06-15'2025-06-15T00:00:00Z (UTC) → 2025-06-14 19:00 (local) → getDate() returns 14, not 15
  • If today is 2025-06-15, a task due on 2025-06-15 would be classified as overdue

The test suite doesn't catch this because it runs with a mocked time in UTC. Both isOverdue() and isToday() have the same vulnerability.

Recommend: Parse date-only strings explicitly as local dates (e.g., new Date(year, month, date) directly from parsing the string components) or use date-fns consistently throughout, which correctly handles local date semantics.

🤖 Fix all issues with AI agents
In `@tdn-desktop/src/lib/deep-link-handler.ts`:
- Around line 296-324: The body-update branch currently swallows failures from
commands.updateTask and still treats the operation as successful; wrap the await
commands.updateTask(...) call in a try/catch (or add error handling where
updateResult.status !== 'ok') and log the error (using the existing logging
facility or console) including context (newTask.id and options.body) so failures
are visible; ensure you still call markMutationStart() before the attempt and
only update queryClient via vaultQueryKeys.task and vaultQueryKeys.tasks() when
updateResult.status === 'ok', and consider returning/propagating a failure
indicator instead of unconditionally returning true.
- Around line 34-37: The hard-coded Sets validNavIds and validDevNavIds can
drift from the NavId and DevNavId type definitions; instead, export canonical
arrays (e.g., NAV_IDS and DEV_NAV_IDS) where NavId/DevNavId are declared and
import them into deep-link-handler.ts, then construct validNavIds = new
Set(NAV_IDS) and validDevNavIds = new Set(DEV_NAV_IDS); also add a unit test
asserting every value of NavId/DevNavId is present in those arrays to ensure
they stay synchronized.
🧹 Nitpick comments (3)
tdn-desktop/src/services/vault/init.ts (1)

57-61: Consider adding .catch() for consistency.

The external change path handles Result.error via .then(), but a promise rejection (e.g., network/IPC failure) would be unhandled. Consider adding .catch() for symmetry with the recent mutation path (lines 44-48).

♻️ Suggested improvement
         // Refresh the vault data from disk
-        commands.refreshVault().then(result => {
-          if (result.status === 'error') {
-            logger.error('Failed to refresh vault', { error: result.error })
-          }
-        })
+        commands.refreshVault()
+          .then(result => {
+            if (result.status === 'error') {
+              logger.error('Failed to refresh vault', { error: result.error })
+            }
+          })
+          .catch(err => {
+            logger.error('Failed to refresh vault', { error: err })
+          })
tdn-desktop/src-tauri/src/commands/recovery.rs (1)

125-136: Guard days-to-seconds conversion against overflow.
Even though the current constant is small, the helper is generic; a saturating multiply prevents wrap in release builds.

♻️ Proposed change
-    let cutoff_secs = now_secs.saturating_sub(days * 24 * 60 * 60);
+    let seconds_per_day = 24_u64 * 60 * 60;
+    let cutoff_secs = now_secs.saturating_sub(days.saturating_mul(seconds_per_day));
tdn-desktop/src/lib/date-utils.ts (1)

70-103: Consider locale-specific short date ordering.

The “further out” path still hard-codes month/day ordering. For locales that prefer day–month or different punctuation, a translation key (or locale formatter) would keep this aligned with i18n expectations.

♻️ Suggested change
-  const monthName = t(monthKeys[monthIndex])
-  return `${monthName} ${date.getDate()}`
+  const monthName = t(monthKeys[monthIndex])
+  return t('dates.shortDate', { month: monthName, day: date.getDate() })

You’d then add dates.shortDate to locale files (e.g., "{{month}} {{day}}" for English, and other locales can reorder).

Introduce parseLocalDate function to correctly parse date-only strings as local dates, preventing timezone issues. Update formatShortDate, formatRelativeDate, isOverdue, and isToday functions to utilize the new parsing method. Improve error handling in vault initialization: Add catch block to log errors when refreshing vault data after external changes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tdn-desktop/src/services/vault/init.ts`:
- Around line 56-71: The code calls commands.refreshVault() but does not await
its completion before calling queryClient.invalidateQueries(vaultQueryKeys.all),
causing refetches against stale data; change the flow to await the promise
returned by commands.refreshVault(), handle both Result status and thrown errors
(as currently done in the .then/.catch), and only call
queryClient.invalidateQueries({ queryKey: vaultQueryKeys.all }) after
refreshVault resolves successfully (or after logging an error), referencing the
existing commands.refreshVault, the returned result.status/error handling, and
queryClient.invalidateQueries(vaultQueryKeys.all).
♻️ Duplicate comments (1)
tdn-desktop/src/services/vault/init.ts (1)

44-48: Handle Result.status in the recent-mutation refresh path.

Line 44 only handles rejections; a successful Result with status: 'error' is still ignored. This is the same issue previously raised for this path. Consider mirroring the error-status check used later in the file.

🔧 Proposed fix
-          commands.refreshVault().catch(err => {
-            logger.error('Failed to refresh vault after recent mutation', {
-              error: err,
-            })
-          })
+          commands
+            .refreshVault()
+            .then(result => {
+              if (result.status === 'error') {
+                logger.error('Failed to refresh vault after recent mutation', {
+                  error: result.error,
+                })
+              }
+            })
+            .catch(err => {
+              logger.error('Failed to refresh vault after recent mutation', {
+                error: err,
+              })
+            })
🧹 Nitpick comments (2)
tdn-desktop/src/lib/date-utils.ts (1)

78-91: Consider simplifying the month index type assertion.

The verbose union type cast spanning 13 lines could be simplified. Since getMonth() always returns 0-11, a single-line assertion would be cleaner.

✨ Suggested simplification
-  const monthIndex = date.getMonth() as
-    | 0
-    | 1
-    | 2
-    | 3
-    | 4
-    | 5
-    | 6
-    | 7
-    | 8
-    | 9
-    | 10
-    | 11
-  const monthName = t(monthKeys[monthIndex])
+  const monthName = t(monthKeys[date.getMonth() as keyof typeof monthKeys])

Alternatively, if the explicit union is preferred for documentation purposes, keep it but consider extracting it as a type alias to reuse between formatShortDate and formatRelativeDate.

tdn-desktop/src/services/vault/init.ts (1)

162-173: Avoid order-sensitive comparison for ignore arrays.

JSON.stringify makes ['a','b'] and ['b','a'] look different. If order isn’t meaningful, this can trigger unnecessary reinitializations.

♻️ Suggested refactor
 export function vaultConfigChanged(
   oldPrefs: AppPreferences | undefined,
   newPrefs: AppPreferences
 ): boolean {
   if (!oldPrefs) return true
+
+  const oldIgnore = [...(oldPrefs.ignore ?? [])].sort()
+  const newIgnore = [...(newPrefs.ignore ?? [])].sort()
+  const ignoreChanged =
+    oldIgnore.length !== newIgnore.length ||
+    oldIgnore.some((value, index) => value !== newIgnore[index])
 
   return (
     oldPrefs.tasksDir !== newPrefs.tasksDir ||
     oldPrefs.areasDir !== newPrefs.areasDir ||
     oldPrefs.projectsDir !== newPrefs.projectsDir ||
-    JSON.stringify(oldPrefs.ignore) !== JSON.stringify(newPrefs.ignore)
+    ignoreChanged
   )
 }

Comment on lines +56 to +71
// Refresh the vault data from disk
commands
.refreshVault()
.then(result => {
if (result.status === 'error') {
logger.error('Failed to refresh vault', { error: result.error })
}
})
.catch(err => {
logger.error('Failed to refresh vault after external change', {
error: err,
})
})

// Invalidate all vault queries to trigger refetches
queryClient.invalidateQueries({ queryKey: vaultQueryKeys.all })
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where refreshVault is defined in the codebase
echo "=== Searching for refreshVault definition ==="
rg -n "refreshVault|refresh_vault" --type ts --type tsx --type rs -g '!**/target/**' -g '!**/node_modules/**'

Repository: dannysmith/taskdn

Length of output: 151


🏁 Script executed:

#!/bin/bash
# Check the tauri-bindings directory structure
echo "=== Checking tauri-bindings ==="
find . -path "*/node_modules" -prune -o -path "*/target" -prune -o -type f \( -name "*tauri-bindings*" -o -name "*bindings*" \) -print | head -20

Repository: dannysmith/taskdn

Length of output: 244


🏁 Script executed:

#!/bin/bash
# Look for tauri command definitions and src-tauri setup
echo "=== Finding tauri command files ==="
fd -type f -name "*.rs" | grep -E "(command|vault)" | head -20

Repository: dannysmith/taskdn

Length of output: 284


🏁 Script executed:

#!/bin/bash
# Search for refreshVault using simpler rg syntax
echo "=== Searching for refreshVault ==="
rg "refreshVault|refresh_vault" -g '!**/target/**' -g '!**/node_modules/**'

Repository: dannysmith/taskdn

Length of output: 644


🏁 Script executed:

#!/bin/bash
# Examine the tauri-bindings file to understand refreshVault
echo "=== Examining tauri-bindings.ts ==="
cat -n ./tdn-desktop/src/lib/tauri-bindings.ts | head -100

Repository: dannysmith/taskdn

Length of output: 1741


🏁 Script executed:

#!/bin/bash
# Look at the Rust bindings to see refreshVault implementation
echo "=== Examining Rust bindings.rs ==="
wc -l ./tdn-desktop/src-tauri/src/bindings.rs

Repository: dannysmith/taskdn

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Examine the Rust refresh_vault implementation
echo "=== Rust refresh_vault implementation ==="
rg -A 20 "pub fn refresh_vault" tdn-desktop/src-tauri/src/commands/vault.rs

Repository: dannysmith/taskdn

Length of output: 807


🏁 Script executed:

#!/bin/bash
# Check the full bindings.ts file to understand the Result wrapper
echo "=== Full bindings.ts to understand Result type and refreshVault ==="
cat -n ./tdn-desktop/src/lib/bindings.ts | grep -A 5 -B 5 "refreshVault"

Repository: dannysmith/taskdn

Length of output: 568


🏁 Script executed:

#!/bin/bash
# Check if queryClient.invalidateQueries is awaited or async
echo "=== Checking invalidateQueries usage in the codebase ==="
rg -B 3 -A 3 "invalidateQueries" tdn-desktop/src/services/vault/init.ts

Repository: dannysmith/taskdn

Length of output: 280


🏁 Script executed:

#!/bin/bash
# Check full context around lines 56-71 in init.ts to understand the event listener structure
echo "=== Full context of vault-changed listener ==="
sed -n '40,85p' tdn-desktop/src/services/vault/init.ts

Repository: dannysmith/taskdn

Length of output: 1503


🏁 Script executed:

#!/bin/bash
# Check if there are other similar patterns in the codebase
echo "=== Other uses of refreshVault to understand intent ==="
rg -B 5 -A 10 "refreshVault" tdn-desktop/src/services/vault/init.ts

Repository: dannysmith/taskdn

Length of output: 1112


🏁 Script executed:

#!/bin/bash
# Check what invalidateQueries returns - is it async?
echo "=== Checking queryClient.invalidateQueries type ==="
rg -B 5 -A 10 "invalidateQueries" tdn-desktop/src/services/vault/init.ts | head -30

Repository: dannysmith/taskdn

Length of output: 403


🏁 Script executed:

#!/bin/bash
# Check TanStack React Query documentation or usage patterns in codebase
echo "=== Other invalidateQueries usages to understand if it's async ==="
rg "invalidateQueries" tdn-desktop/src -A 2 -B 2 | head -50

Repository: dannysmith/taskdn

Length of output: 471


Await refreshVault() before invalidating queries to prevent stale data refetches.

invalidateQueries() executes synchronously right after refreshVault() returns its promise, before the Rust vault refresh actually completes. This causes queries to refetch against stale data. Await refreshVault() and check its Result status before invalidating.

🔧 Proposed fix
-        // Refresh the vault data from disk
-        commands
-          .refreshVault()
-          .then(result => {
-            if (result.status === 'error') {
-              logger.error('Failed to refresh vault', { error: result.error })
-            }
-          })
-          .catch(err => {
-            logger.error('Failed to refresh vault after external change', {
-              error: err,
-            })
-          })
-
-        // Invalidate all vault queries to trigger refetches
-        queryClient.invalidateQueries({ queryKey: vaultQueryKeys.all })
+        // Refresh the vault data from disk before invalidating queries
+        ;(async () => {
+          try {
+            const result = await commands.refreshVault()
+            if (result.status === 'error') {
+              logger.error('Failed to refresh vault', { error: result.error })
+              return
+            }
+            await queryClient.invalidateQueries({ queryKey: vaultQueryKeys.all })
+          } catch (err) {
+            logger.error('Failed to refresh vault after external change', {
+              error: err,
+            })
+          }
+        })()
📝 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
// Refresh the vault data from disk
commands
.refreshVault()
.then(result => {
if (result.status === 'error') {
logger.error('Failed to refresh vault', { error: result.error })
}
})
.catch(err => {
logger.error('Failed to refresh vault after external change', {
error: err,
})
})
// Invalidate all vault queries to trigger refetches
queryClient.invalidateQueries({ queryKey: vaultQueryKeys.all })
// Refresh the vault data from disk before invalidating queries
;(async () => {
try {
const result = await commands.refreshVault()
if (result.status === 'error') {
logger.error('Failed to refresh vault', { error: result.error })
return
}
await queryClient.invalidateQueries({ queryKey: vaultQueryKeys.all })
} catch (err) {
logger.error('Failed to refresh vault after external change', {
error: err,
})
}
})()
🤖 Prompt for AI Agents
In `@tdn-desktop/src/services/vault/init.ts` around lines 56 - 71, The code calls
commands.refreshVault() but does not await its completion before calling
queryClient.invalidateQueries(vaultQueryKeys.all), causing refetches against
stale data; change the flow to await the promise returned by
commands.refreshVault(), handle both Result status and thrown errors (as
currently done in the .then/.catch), and only call
queryClient.invalidateQueries({ queryKey: vaultQueryKeys.all }) after
refreshVault resolves successfully (or after logging an error), referencing the
existing commands.refreshVault, the returned result.status/error handling, and
queryClient.invalidateQueries(vaultQueryKeys.all).

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.

2 participants