Commit Graph

221 Commits

Author SHA1 Message Date
lucaronin
761d6dc3b5 fix: rename_note handles filename-slug mismatch and collisions
When the note content already has the correct title but the filename
doesn't match (e.g. untitled-note-9.md after user changed H1), the
rename was a no-op. Now checks both title AND filename slug before
early-returning. Also uses unique_dest_path for collision handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:09:55 +01:00
lucaronin
7005f54476 fix: mock move_note_to_type_folder collision handling + Rust collision content test
The mock handler now appends -2, -3, etc. when the target path already
exists, matching the Rust unique_dest_path logic.  Previously it would
silently overwrite the existing note's content in MOCK_CONTENT.

Also adds a Rust test that verifies both the moved note and the
pre-existing note retain their respective content after a collision.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:39:18 +01:00
lucaronin
8a8fa802bb style: rustfmt formatting fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:09:36 +01:00
lucaronin
4c7e64dc0c fix: bump cache version + handle Yes/No in TS frontmatter parser
Root cause: commit d6d35b3 added a custom Rust deserializer for
Archived/Trashed Yes/No strings but did not bump CACHE_VERSION.
Existing vaults had stale cached entries with archived: false from the
old parser, and since the cache version (5) matched, stale values were
served without re-parsing from disk.

- Bump CACHE_VERSION 5 → 6 to force full rescan on next vault load
- Add Yes/No handling to TypeScript parseScalar (Inspector display)
- Add integration tests: cached vault path with Archived/Trashed: Yes
- Add stale cache version invalidation test
- Add frontmatter.test.ts for TS Yes/No boolean parsing
- Add Playwright smoke test for archived note filtering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:07:22 +01:00
lucaronin
3084702e5c fix: prevent crash in handleCreateNoteImmediate — slugify fallback + try/catch
- slugify now returns 'untitled' instead of empty string when input has only
  special characters, preventing invalid paths like '/vault//note.md'
- handleCreateNoteImmediate wrapped in try/catch — worst case shows a toast
  error instead of crashing the app
- Added Rust test for deeply nested directory creation in save_note_content
- Regression tests for slugify edge cases and handleCreateNoteImmediate with
  special-character types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:50:05 +01:00
lucaronin
a760594d66 style: apply cargo fmt to is_file_trashed tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:44:37 +01:00
lucaronin
73060f3d00 fix: exclude trashed notes from search results and autocomplete
Trashed notes were appearing in search (Cmd+F), Quick Open (Ctrl+P),
wikilink autocomplete ([[), and person mention autocomplete (@).

Rust: add is_file_trashed() to check frontmatter, filter search_vault results.
Frontend: filter trashed entries from useNoteSearch, baseItems in both
editor views, and mock search_vault handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:42:56 +01:00
lucaronin
d6d35b3057 fix: handle Archived/Trashed Yes/No string values in frontmatter parser
The Rust YAML parser only accepted boolean values (true/false) for the
archived and trashed fields. When the vault writes Archived: Yes or
Trashed: Yes (YAML string, not boolean), serde silently returned None
and the note appeared as non-archived/non-trashed.

Add a custom deserializer that accepts both booleans and string
representations (Yes/yes/YES/true/1 → true, No/no/false/0 → false).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:59:27 +01:00
lucaronin
656679e21a style: apply cargo fmt to new tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:14:34 +01:00
lucaronin
ba5ba58c6b fix: reload vault invalidates cache and rescans from filesystem
Reload Vault (Cmd+K) now calls the new `reload_vault` Tauri command which
deletes the cache file before scanning, guaranteeing a full filesystem
rescan. Previously it called `list_vault` which used incremental git-based
cache updates that could miss recent changes (e.g. trashing a note then
reloading showed stale data).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:14:34 +01:00
lucaronin
664426d598 fix: prune stale cache entries on vault open, not just cache write
Remove the early return in update_same_commit that skipped filesystem
validation when git reported no changes. Add prune_stale_entries to
finalize_and_cache so every vault scan path validates entries exist on
disk and deduplicates by case-folded path. Prevents ghost notes after
deleting files outside the app (e.g., via Finder).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 03:06:55 +01:00
lucaronin
fdb3fdf6fe style: rustfmt vault/mod.rs test formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:52:56 +01:00
lucaronin
dedb5b2721 feat: wire Reload Vault into Cmd+K palette and menu bar
Adds a "Reload Vault" command that forces a full rescan from filesystem,
bypassing cache. Available via Cmd+K and Vault menu. Wired through
useAppCommands → useCommandRegistry and useMenuEvents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:52:56 +01:00
lucaronin
587eb0231e feat: add reload_vault_entry Tauri command
Re-reads a single .md file from disk and returns a fresh VaultEntry.
Used after failed optimistic updates to restore the true filesystem state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:52:56 +01:00
lucaronin
55e479be13 feat: flip canonical type field — make type: primary, Is A: the alias
The Rust parser now treats `type:` as the canonical frontmatter field
for entity type, with `Is A:` and `is_a:` accepted as legacy aliases.
Previously it was the other way around, creating an asymmetric
read/write cycle since the frontend and all 8800+ vault notes already
use `type:`.

- Flip serde attribute: rename="type", alias="Is A", alias="is_a"
- Update theme defaults, getting-started vault, and type definitions
- Add round-trip tests for both type: and Is A: parsing
- Update mock data and TypeScript tests to use canonical form

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:38:17 +01:00
lucaronin
02611f6d41 feat: move vault cache to ~/.laputa/cache/ and make writes atomic
Cache files are now stored outside the vault directory at
~/.laputa/cache/<vault-hash>.json, preventing them from polluting
the user's git repo. Writes use atomic tmp+rename to avoid corruption.
Legacy .laputa-cache.json files are auto-migrated and cleaned up on
first run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:04:14 +01:00
lucaronin
dd1595cf68 style: apply rustfmt to rename.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:52:30 +01:00
lucaronin
2d2c162c3c feat: add move_note_to_type_folder backend command
Adds a new Tauri command that moves a note file to the folder
corresponding to its new type when Is A is changed. Handles:
- folder creation, filename collision (-2 suffix), wikilink updates,
  and no-op when already in the correct folder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:52:30 +01:00
lucaronin
f2a9f02d2a fix: resolve AI chat empty body via || fallback + defensive body + Rust strip_frontmatter
Three root causes identified and fixed:

1. JS semantics bug: `activeNoteContent ?? allContent[path]` used nullish
   coalescing (`??`) which does NOT fall through on empty string ''. When
   handleEditorChange temporarily overwrites tab.content with frontmatter-
   only content during async content swaps, activeNoteContent becomes ''
   and the fallback to allContent never triggers. Fix: change `??` to `||`.

2. Defence-in-depth: when body is still empty after fallback (Tauri mode
   where allContent is {}), but wordCount > 0, the body field now includes
   an explicit get_note instruction instead of being empty. This is more
   reliable than the preamble instruction that Claude may skip.

3. Rust strip_frontmatter used `rest.find("---")` which matches `---`
   anywhere in text (including inside frontmatter values like `title:
   foo---bar`). Fixed to use `rest.find("\n---")` for line-boundary
   matching. This ensures accurate wordCount for the fallback heuristic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 17:58:47 +01:00
lucaronin
78954d863d style: rustfmt assert formatting 2026-03-08 16:40:52 +01:00
lucaronin
3c711cbe04 fix: parse lowercase 'archived' frontmatter field
The frontend writes 'archived: true' (lowercase) via handleUpdateFrontmatter,
but the Rust parser only recognized 'Archived' (titlecase). This caused all
notes archived from within Laputa to be read back as not archived — they
continued appearing in the sidebar and note list after restart.

Fix: add alias = "archived" to the serde attribute, matching the pattern
already used for 'trashed'/'Trashed'.

Regression tests added for both lowercase and titlecase variants.
2026-03-08 16:40:52 +01:00
lucaronin
e29360ef0a style: cargo fmt config_seed.rs and getting_started.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:46:43 +01:00
lucaronin
2d414961a9 fix: clippy doc_lazy_continuation in config_seed.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:45:09 +01:00
lucaronin
c1e11f45df feat: add Repair Vault command and MCP configFiles
- Add "Repair Vault" to command palette (Cmd+K → "Repair Vault")
- Add "Repair Vault" to macOS Vault menu bar
- Wire repair_vault Tauri command through App → useAppCommands → registry
- Add menu event handler for vault-repair
- Update MCP get_vault_context to include configFiles.agents content
- Add repair_vault mock handler for browser testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:39:55 +01:00
lucaronin
50e4a63ef2 feat: add config/ vault type with agents.md migration
- Add config_seed module: seed_config_files, migrate_agents_md, repair_config_files
- New vaults seed config/agents.md + root AGENTS.md stub (Codex compat)
- Existing vaults auto-migrate root AGENTS.md → config/agents.md on open
- Add type/config.md (icon: gear-six, sidebar label: Config)
- Add "config" → "Config" folder type mapping
- Add repair_vault Tauri command (themes + config files)
- 24 new tests covering seeding, migration, repair, idempotency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:35:52 +01:00
lucaronin
f2ec2ab55c feat: make MCP restore command always available in Cmd+K
The "Install MCP Server" command was only enabled when status was
"not_installed", preventing users from re-registering when MCP got
removed or broken. Now the command is always available:
- Shows "Install MCP Server" when not installed
- Shows "Restore MCP Server" when already installed
- Added restore/fix/repair keywords for discoverability
- Context-aware toast: "installed" vs "restored"
- Menu bar label updated to "Restore MCP Server"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:33:18 +01:00
lucaronin
5324f729a9 fix: scope qmd update/embed to current vault collection only
Previously called 'qmd update' and 'qmd embed' without arguments, which
updated all global qmd collections. If any other collection had a broken
path, the entire indexing would fail.

Now passes the vault name explicitly:
  qmd update <vault_name>
  qmd embed -c <vault_name>

This makes reindex independent of other qmd collections on the system.
2026-03-07 10:08:39 +01:00
lucaronin
36065b0ca5 fix: clippy errors — reduce visibility of internal functions, fix PI approx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:19:45 +01:00
lucaronin
1dccc0239c feat: reindex vault command, indexing status bar, sync-triggered reindex
- Add "Reindex Vault" command to command palette and menu bar
- Show "Indexed Xm ago" in status bar when idle, clickable to reindex
- After git pull with updates, auto-trigger incremental reindex
- Add lastIndexedTime state to useIndexing, populated from backend metadata
- Add triggerFullReindex to useIndexing (retryIndexing is now an alias)
- Add onSyncUpdated callback to useAutoSync
- Extract formatIndexedElapsed to utils/indexingHelpers.ts
- Tests: 20 new unit tests across 5 files, 2 Playwright smoke tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:14:12 +01:00
lucaronin
47fe307d45 feat: add last_indexed_commit persistence to indexing backend
Store last_indexed_commit and last_indexed_at in .laputa-index.json
after every successful full or incremental index. Include these in
IndexStatus so the frontend can display staleness. Add
needs_reindex_after_sync() helper that compares HEAD vs stored commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:00:58 +01:00
lucaronin
4c28fa8e25 fix: rustfmt import formatting in commands.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:38:08 +01:00
lucaronin
a4dac661ac feat: add GitPushResult with error classification for push failures
Replaces raw string return from git_push with a structured GitPushResult
that classifies errors as rejected/auth_error/network_error/error, each
with an actionable user-facing message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:29:54 +01:00
lucaronin
bd6487c1d2 style: rustfmt formatting fix in cache test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:22:33 +01:00
lucaronin
875d8177a4 fix: add visible key to frontmatterToEntryPatch maps
The ENTRY_DELETE_MAP and update map in frontmatterToEntryPatch were
missing the 'visible' key. When handleDeleteProperty or
handleUpdateFrontmatter was called for 'visible', the in-memory
VaultEntry was not updated, causing the sidebar to stay stale even
after the file on disk was correctly modified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:20:51 +01:00
lucaronin
95f6c05053 refactor: remove Favorites and Untagged from sidebar
- Remove Favorites NavItem, Star icon import, go-favorites command
- Remove Untagged NavItem, TagSimple icon import
- Remove favorites from SidebarFilter union type
- Update tests: Sidebar.test.tsx, useMenuEvents.test.ts, App.test.tsx
- Remove GO_FAVORITES constant and menu item from menu.rs
2026-03-06 23:19:37 +01:00
lucaronin
3f6b5b9a06 refactor: rustfmt formatting fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:15:24 +01:00
lucaronin
ef4208697e style: rustfmt vault_config.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:58:39 +01:00
lucaronin
486b0e9ed1 feat: migrate hidden_sections from ui.config to visible property on Type notes
On startup, reads hidden_sections from config/ui.config.md, creates or
updates Type notes with visible: false, then re-saves config without
hidden_sections. Idempotent and safe to run multiple times.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:44:30 +01:00
lucaronin
4df386d310 refactor: remove hidden_sections from VaultConfig and delete useSectionVisibility
Sidebar section visibility is now controlled entirely by the `visible`
property on Type notes. Removes all hidden_sections references from
Rust struct, TypeScript interface, config migration, mock data, and tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:41:00 +01:00
lucaronin
a7d94b354e feat: add visible field to VaultEntry for Type note sidebar visibility
Parse `visible` boolean from Type note frontmatter (defaults to None/true
when absent). Add to SKIP_KEYS to exclude from relationships and properties.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:30:18 +01:00
lucaronin
1817cb3794 🐛 Auto-untrack .laputa-cache.json and .laputa/settings.json from git
These are machine-local files that should never be version controlled:
- .laputa-cache.json: contains absolute paths, changes on every machine
- .laputa/settings.json: per-machine UI settings

Fix: ensure_cache_excluded() now:
1. Adds both files to .git/info/exclude (git-level ignore, no .gitignore needed)
2. Runs `git rm --cached --ignore-unmatch` on vault open to un-track them
   if they were committed in older vaults

This is idempotent and self-healing — existing vaults fix themselves
automatically on next app launch without any manual steps.
2026-03-06 21:10:37 +01:00
lucaronin
66f58446b4 Pulse: lazy pagination with IntersectionObserver infinite scroll
- Page size reduced from 30 → 20 commits per fetch
- Backend: add `skip` param to get_vault_pulse (git log --skip)
  → true pagination instead of re-fetching everything
- Frontend: IntersectionObserver sentinel at bottom of feed
  → auto-loads next page when user scrolls near end
- Append-only updates (no full re-render on load more)
- Add IntersectionObserver mock to test setup
2026-03-06 21:04:24 +01:00
lucaronin
99faee6773 fix: address clippy lints in title_case_folder
Use char array pattern and function reference instead of closures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:11:57 +01:00
lucaronin
40b5d31003 fix: normalize hyphenated folder names in infer_type_from_folder()
Split on hyphens and underscores, capitalize each word, join with
spaces. E.g. monday-ideas → Monday Ideas, key-result → Key Result.
This prevents duplicate sidebar sections when folder name differs
from the Type file title.

Bump CACHE_VERSION to 5 to force rescan on existing caches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:59:51 +01:00
lucaronin
bfc79c870b refactor: apply rustfmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:43:43 +01:00
lucaronin
1c2552a1bb fix: vault cache misses files in new directories, breaking theme restore
Root cause: git_uncommitted_files() used `git status --porcelain` which
reports new untracked directories as `?? theme/` instead of listing
individual files inside. The .md filter skipped directory entries, so
files in newly-created directories (theme/default.md, etc.) were
invisible to the cache system.

Fix: additionally use `git ls-files --others --exclude-standard` which
lists individual untracked files, resolving directories to their
contents.

Also:
- Add type/theme.md definition (icon: palette) to restore and getting-started vault
- Seed theme/*.md vault notes in getting-started vault
- Fix mock create_vault_theme to add entries for dev mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:38:54 +01:00
lucaronin
d7c210222c fix: exclude .laputa-cache.json and settings.json from vault git tracking
These files are machine-specific and should never be committed or cause
conflicts when syncing vaults across devices.

Changes:
- init_repo() now writes .gitignore with .laputa-cache.json and
  .laputa/settings.json excluded before the first commit
- .DS_Store and common editor artifacts also excluded
- openConflictFileRef falls back to openLocalFile() for non-note files
  so 'Open in editor' works for .json conflict files (opens in system
  default app, e.g. TextEdit/VS Code)
- Removed stale git.rs (replaced by git/mod.rs from refactor)
- 2 new Rust tests for .gitignore creation behavior
2026-03-06 15:22:48 +01:00
lucaronin
8712ce8e13 fix: write .gitignore on vault init to exclude .DS_Store
macOS creates .DS_Store files in every folder which were being tracked
by git, causing constant conflicts when vaults are synced across machines.

init_repo() now writes a .gitignore before the first commit with:
- .DS_Store, .AppleDouble, .LSOverride
- ._ thumbnail files
- Common editor artifacts (.vscode/, .idea/, *.swp)

The file is only written if one doesn't already exist, so user-customized
.gitignore files are respected.

Two new Rust tests verify the behavior.
2026-03-06 15:19:25 +01:00
lucaronin
f4af2b4b19 refactor: split Rust backend into sub-modules — git, theme, github, frontmatter, commands
- git.rs (1907 lines) → 7 files: mod, history, status, commit, remote, conflict, pulse
- theme.rs (1075 lines) → 4 files: mod, defaults, seed, create
- github.rs (886 lines) → 4 files: mod, api, auth, clone
- frontmatter.rs (827 lines) → 3 files: mod, yaml, ops
- lib.rs (772 lines) → lib.rs (160) + commands.rs (650)

484 tests pass, clippy clean, rustfmt clean, coverage 85.42%
2026-03-06 13:16:32 +01:00
lucaronin
817055a9e1 fix: make qmd/search work on fresh installs — auto-install, fix permissions, sign binaries
On fresh MacBook installs, the bundled qmd binary fails to run due to:
missing execute permissions, macOS quarantine attributes, and no fallback
when qmd is completely absent. This fix addresses all three issues:

- Runtime: ensure +x permissions and remove quarantine on bundled qmd
- Runtime: auto-install qmd via bun when binary not found anywhere
- Build: ad-hoc code-sign qmd and .dylib files in bundle-qmd.sh
- Build: create placeholder resource dirs so fresh clones build cleanly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:13:03 +01:00