- Rust Frontmatter struct now parses both 'type' (new) and 'Is A'/'is_a' (legacy),
with 'type' taking precedence via resolve_type()
- Added migrate_is_a_to_type() vault-wide migration (runs on startup, also exposed as Tauri command)
- Frontend: buildNoteContent and resolveNewType now emit 'type:' in YAML
- Inspector SKIP_KEYS hides 'is_a', 'Is A', 'type', 'title' from property list
(TypeRow already renders the type with its dedicated UI)
- frontmatterToEntryPatch handles both 'type' and legacy 'is_a' keys
- Mock data updated: all YAML content uses 'type:' instead of 'is_a:'/'Is A:'
- Tests updated to expect 'type:' in generated frontmatter
Product decision: internal VaultEntry field stays as `isA` (TS) / `is_a` (Rust)
to minimize blast radius. The user-facing change is the YAML key and inspector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removes the debounced auto-save (useAutoSave hook) which was causing the
editor to reload previous content. Replaces it with explicit save on
Cmd+S (⌘S), consistent with the git-based UX of the app.
- Removed useAutoSave hook and its debounce mechanism
- Added useSaveNote hook for direct persist-to-disk
- Added useEditorSave hook that manages pending content buffer + save
- Cmd+S now persists the current editor content immediately
- Rename-before-save: saves pending content before rename to prevent
the "Failed to rename note" error
- Updated E2E test to verify Cmd+S behavior
- 471 tests passing, code health gates passed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The is_a frontmatter key was appearing both as the TypeRow badge and
as a regular editable property. Added both 'is_a' and 'Is A' variants
to SKIP_KEYS to prevent duplication (snake_case used in mock data,
space-separated used in real vault YAML).
Added test verifying is_a is skipped when TypeRow is present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split the Properties panel into two visually distinct sections:
1. **Properties** (editable): frontmatter properties with interactive
hover background, cursor pointer, and click-to-edit. Includes Type
row, Status, Owner, tags, and all user-defined properties.
2. **Info** (read-only): derived metadata shown below a border
separator with an "Info" header. Uses muted color (--text-muted)
with no hover states or click interaction. Includes:
- Modified date
- Created date (new)
- Word count
- File size (new, formatted as B/KB/MB)
Product decisions:
- Info section uses --text-muted for both labels and values (more
dimmed than the --muted-foreground used by editable labels)
- Editable rows get rounded hover:bg-muted on the full row
- Type row stays at top of Properties as an identity element
- Added Created date and file Size to give more useful metadata
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix type annotations in App.tsx (setTabs callback)
- Cast restoreWikilinksInBlocks result in Editor.tsx for BlockNote compatibility
- Fix vi.fn() mock types in useAutoSave.test.ts
- Add E2E test verifying editor loads and renders note content
- Update .claude-done with editor-save-bug summary
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The editor was not persisting changes to disk. Edits would be lost
when closing and reopening a note.
Changes:
- Add save_note_content Tauri command (Rust) with read-only file check
- Add save_note_content mock handler for browser testing
- Add useAutoSave hook with 500ms debounce and flush-on-tab-switch
- Wire up BlockNote onChange → markdown serialization → auto-save
- Add restoreWikilinksInBlocks utility (reverse of injectWikilinks)
to convert wikilink nodes back to [[target]] before markdown export
- Extract shared walkBlocks helper to eliminate code duplication
- Suppress onChange during programmatic content swaps (tab switching)
- 12 new frontend tests + 5 new Rust tests (all 469 + 174 passing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Changed view mode shortcuts from Alt+1/2/3 to Cmd+1/2/3 (Alt+N produces
special chars on macOS, e.g. Alt+1 → '¡')
- menu.rs: use MenuItemBuilder with .accelerator() instead of appending
shortcut text to label (was purely decorative, not registered with OS)
- Shortcuts now: CmdOrCtrl+1 editor-only, +2 editor+list, +3 all panels
- getSortComparator with explicit asc/desc direction for all sort modes
- Direction arrows visible in dropdown menu
- Clicking direction arrow reverses sort order in the list
- Direction persistence verified through sort behavior
- Direction icon shown on sort button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add SortDirection type and SortConfig interface to noteListHelpers
- getSortComparator now accepts optional direction parameter
- SortDropdown shows ↑/↓ arrow buttons per option in the menu
- Button label shows current direction arrow (ArrowUp/ArrowDown)
- Persistence updated to store {option, direction} with backward compat
for old string-only preferences
- Default directions: modified/created=desc, title/status=asc
- Status sort tiebreaker always uses newest-first regardless of direction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added AnyBlock type alias to cast injectWikilinks() return type in tests.
The function returns unknown[] but tests access .content, .props, .text
properties — this was already broken before this branch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 tests covering:
- Renders nothing when closed
- Shows "connect account" prompt without token
- Opens settings from no-token state
- Shows clone/create tabs with token
- Loads and displays repo list from mock
- Filters repos by search term
- Selects repo and auto-fills clone path
- Disables clone button without selection
- Calls clone_repo and onVaultCloned on successful clone
- Create tab trigger is visible and clickable
- Shows error message on clone failure
- Shows Private/Public badges on repos
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New GitHubVaultModal component with two tabs:
- Clone Existing: search/browse user's repos, select one, choose
local path, clone via HTTPS+token
- Create New: enter repo name, toggle private/public, create via
GitHub API then clone locally
Integration:
- "Connect GitHub repo" option in vault switcher menu
- GitHub token field added to Settings panel
- Cloned vaults auto-added to vault list and opened
- Mock handlers for github_list_repos, github_create_repo, clone_repo
- All existing tests updated for new github_token field (386 pass)
Product decision: GitHub token stored in settings rather than OAuth
flow — simpler first version, works with personal access tokens.
Users can upgrade to OAuth later.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New github.rs module with three commands:
- github_list_repos: lists user's repos via GitHub API (paginated)
- github_create_repo: creates a new repo with auto_init
- clone_repo: git clone with HTTPS+token auth, configures remote
Also extends Settings with github_token field (Rust + TypeScript),
and adds GithubRepo type to shared types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Editor.tsx: use targeted eslint-disable for BlockNote block arrays
(PartialBlock generic type is extremely complex to reference)
- Editor.tsx: cast through unknown for tryParseMarkdownToBlocks result
which may be sync or async
- useMcpBridge.ts: cast Promise resolve to (value: unknown) => void
- mock-tauri.ts: cast args.path to string for encodeURIComponent
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap ref assignments in useEffect to fix refs-during-render in
useTabManagement, useKeyboardNavigation, and Editor
- Rewrite useAppKeyboard to define keyMap inside useEffect, eliminating
ref access during render
- Add eslint-disable for react-hooks/set-state-in-effect on legitimate
dialog reset patterns (CommitDialog, CreateNoteDialog, CreateTypeDialog,
QuickOpenPalette, AIChatPanel, useVaultLoader)
- Add eslint-disable for react-hooks/static-components on icon lookups
(NoteItem, NoteList PinnedCard) — stateless icon components from a
static map
- Add eslint-disable for react-hooks/purity on Date.now() in
NoteItem TrashDateLine — intentionally memoized on trashedAt
- Remove unused `model` param from buildSystemPrompt (ai-chat.ts) and
update callers
- Fix exhaustive-deps: suppress vault object dep in App.tsx git history
effect (vault object is unstable, loadGitHistory is the real dep)
- Remove unused `modifiedFiles` from useNoteListData params and caller
- Add missing `ref` to Sidebar useOutsideClick deps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Test mocks: use { children?: React.ReactNode } and Record<string, unknown>
- Editor.tsx: use unknown[] for BlockNote block arrays
- wikilinks.ts: add BlockLike/InlineItem interfaces for block processing
- useMcpBridge.ts: use unknown for resolve/reject and Record<string, unknown>
- ai-chat.ts: use unknown catch + instanceof Error check
- mock-tauri.ts: type messages array, use Record<string, unknown> for args
(keep one targeted eslint-disable for heterogeneous handler map)
- vite.config.ts: use http.IncomingMessage/ServerResponse, typed messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The macOS window frame (transparent title bar, 38px) and the sidebar
share the same background color (#F7F6F3), making them visually
indistinguishable. Replace the sidebar's paddingTop: 38 with a
dedicated spacer div that has a bottom border (1px solid var(--border)),
matching the border style used between all other panels. The spacer
also serves as a drag region for window movement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Install @tauri-apps/plugin-updater + plugin-process (npm + cargo)
- Configure tauri.conf.json: updater endpoint pointing to GitHub Releases,
signing pubkey, createUpdaterArtifacts enabled
- Register updater + process plugins in Rust (desktop-only, #[cfg(desktop)])
- Add updater:default + process:default capabilities
- Add useUpdater hook: checks for updates 3s after startup, shows native
confirm dialog, downloads+installs+relaunches on accept
- Silently catches errors so update failures never block the app
- Add comprehensive tests for the useUpdater hook (5 test cases)
Product decision: Using window.confirm() for the update dialog rather than
a custom React modal. This keeps the implementation simple and the native
dialog is appropriate for a system-level action like app restart. Can be
upgraded to a custom UI later if desired.
Private key generated at ~/.tauri/laputa.key — NOT committed.
For CI: set TAURI_SIGNING_PRIVATE_KEY + TAURI_SIGNING_PRIVATE_KEY_PASSWORD
as GitHub Secrets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused cn import from NoteList.tsx
- Fix SidebarSelection type narrowing in NoteList entity view
- Fix SortableSection onToggle type in Sidebar Omit type
- Fix TypeRow isA prop to accept null
- Align useEntryActions handleUpdateFrontmatter value type
- Add missing trashed/trashedAt/order to test mock VaultEntry objects
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split Inspector.tsx (score 7.78) into:
- Inspector.tsx: slim shell with header, layout, prop wiring (score 9.6)
- InspectorPanels.tsx: DynamicRelationshipsPanel, BacklinksPanel,
GitHistoryPanel, LinkButton, RelationshipGroup (score 8.87)
Extracted LinkButton as reusable component for both relationship
refs and backlinks. Moved resolveRefProps to simplify RelationshipGroup.
Extracted AddRelationshipForm and extractRelationshipRefs as focused units.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>