Root causes:
- toolInputMapRef in useAiAgent was overwritten by tool_progress events
(which arrive with input=undefined AFTER the assistant message set
the full input), causing detectFileOperation to receive undefined
and skip file creation detection entirely.
- MCP open_note only broadcast open_tab without vault_changed, so
the note list didn't refresh when Claude Code called open_note.
- detectFileOperation only handled Write/Edit but not Bash commands
that create .md files via redirects.
Fixes:
- Preserve accumulated input in toolInputMapRef (input ?? prev?.input)
- MCP open_note now broadcasts vault_changed before open_tab
- detectFileOperation now detects Bash redirect patterns (>, >>, tee)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The AI chat was using both --resume (CLI session resumption) AND formatted
conversation history in the prompt simultaneously. This dual-context approach
confused the model — it saw the conversation twice (from session + from prompt
markup), leading to "I don't have context" responses on follow-ups.
Fix: remove --resume entirely from chat mode. Each CLI call is now independent,
with full conversation history formatted into the prompt via
<conversation_history> markup. trimHistory handles graceful truncation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add highlight_editor and refresh_vault tools to the MCP stdio server
so Claude Code can visually highlight UI elements and trigger vault
rescans. Also fix outdated test.js imports after the ai-agent-full-shell
simplification removed write operations from vault.js.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update mock agent response to include [[wikilinks]] for testing.
Add smoke test verifying wikilinks render as clickable elements
with correct text, attributes, and styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- System prompts instruct AI to use [[Note Title]] wikilink syntax
- preprocessWikilinks converts [[Target]] to markdown links
- Custom urlTransform allows wikilink:// scheme through sanitizer
- Click handler resolves target via findEntryByTarget and opens note
- Styled as colored chips matching primary accent
- Works in both AiPanel (agent) and AIChatPanel (legacy chat)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
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>
Expose mockHandlers on window for Playwright overrides. Test that
rejected push shows "Pull first" message, auth error shows
"authentication error", and success shows "Committed and pushed".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update commitWithPush to parse GitPushResult from backend and show
specific messages (rejected, auth, network) instead of generic
"push failed". Tests verify rejected + network error scenarios.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
- Add trimHistory() to keep most recent messages within 100k token budget
- Add formatMessageWithHistory() to prepend conversation context to each message
- Wire up in useAIChat.ts: sendMessage now includes full history
- Keep --resume as belt-and-suspenders for same-session continuity
- 14 new tests in ai-chat.test.ts and useAIChat.test.ts
Also track mock frontmatter writes in mockSavedSinceCommit so the
Changes panel updates correctly in browser dev mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Trash, archive, restore, and unarchive wrote frontmatter to disk but
never called loadModifiedFiles, so the change didn't appear in the
Changes panel until the next manual refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: clicking a note in Pulse used an inline arrow function that:
1. Was recreated on every render (new prop ref → PulseView memo bypassed)
2. Called vault.reloadVault() (full 9000-note rescan) when path didn't match
Fix:
- Add entriesByPath Map (useMemo) — O(1) lookups instead of O(n) .find()
- Add handlePulseOpenNote (useCallback) — stable ref, never triggers reloadVault
(Pulse notes always exist in vault; no reload needed)
- Wire PulseView to handlePulseOpenNote instead of inline arrow
- Also use entriesByPath in openNoteByPath (MCP bridge)
Also updates vite.config.ts vault API parser to include all VaultEntry
fields (visible, icon, color, order, etc.) so the dev server returns
complete entries for sidebar filtering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
Toggle visible property on Type note frontmatter: sets visible:false
to hide, deletes visible property to show (defaults to visible).
Wire up in App.tsx via onToggleTypeVisibility prop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace useSectionVisibility hook with direct filtering on
typeEntryMap[type]?.visible !== false. Add onToggleTypeVisibility
callback prop. Tests updated to verify visible:false hides sections.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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.
- 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
- Add border-r border-[var(--sidebar-border)] to PulseView container
- CommitCard files default to collapsed (expanded: false) for cleaner initial state
- Update tests to reflect new collapsed-by-default behavior
- Sidebar unit test: entries with isA 'Monday Ideas' produce exactly
one section header (not two)
- Playwright smoke test: verify no duplicate section labels in sidebar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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
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.
tools/qmd/ contains vendored qmd source code (not our code) that was
dragging the overall code health score down to 6.53.
scripts/ contains one-shot utility scripts, not production code.
Excluding both from CodeScene so the health metric reflects only
the actual app codebase (src/, src-tauri/src/).
The Tauri updater plugin requires the .app.tar.gz artifact as the update
URL — not the .dmg installer. The DMG is for fresh installs only.
This was causing 'Install Update' to silently fail on all macOS builds
since the auto-updater infrastructure was set up.
Change ARM_DMG → ARM_TARBALL, pointing to the .app.tar.gz artifact.
Adds headless Chromium smoke tests that run before push, catching
UI/UX bugs before Brian QA. Includes shared helpers for command
palette and keyboard shortcut testing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bundle-qmd.sh signs qmd/qmd and vec0.dylib with Developer ID + hardened
runtime, but the certificate wasn't in the keychain yet when it ran
(Tauri imports the cert internally, AFTER beforeBuildCommand completes).
Fix: add explicit 'Import Apple Developer certificate' step before the
Tauri build step, using security create-keychain + security import.
This makes the cert available to codesign during beforeBuildCommand.
Apple notarization rejected qmd/qmd and qmd/vec0.dylib due to:
- Not signed with valid Developer ID certificate
- Hardened runtime not enabled
Changes:
- bundle-qmd.sh: use APPLE_SIGNING_IDENTITY + --options runtime --timestamp in CI
(falls back to ad-hoc signing in dev when no identity is set)
- useThemeManager.test.ts: add ensure_vault_themes to mock (added by theme
editor feature, missing from stale theme ID test mock)
Claude Code must now run Playwright smoke tests against the dev server
before firing the done signal. This catches UI/UX bugs (missing Cmd+K
commands, broken shortcuts, layout issues) before Brian's native QA.
- Phase 1 (Playwright, headless) = Claude Code's responsibility
- Phase 2 (native Tauri, keyboard-only) = Brian's responsibility
Phase 1 covers: command palette entries, keyboard shortcuts, Tab
navigation, UI state changes. Phase 2 covers: file system, git, native
Tauri behaviors that can't run in the browser.
bundle-qmd.sh was trying to install qmd via 'bun install -g qmd' which
installs a different public npm package, not Luca's qmd tool. CI runners
(runner user) don't have the local qmd installation.
Fix:
- Copy qmd source (src/, package.json, tsconfig.json, bun.lock) to tools/qmd/
- Update bundle-qmd.sh to prefer tools/qmd/ as QMD_SRC
- Run 'bun install --frozen-lockfile' in QMD_SRC if node_modules missing
- Update sqlite-vec lookup to find packages from node_modules after bun install
- Compilation uses 'cd $QMD_SRC && bun build --compile src/qmd.ts'
- Add tools/ to eslint globalIgnores (qmd source has its own lint standards)
- Local dev machines still work (tools/qmd/ takes priority over global install)
bundle-qmd.sh uses bun to compile the qmd binary. Release runners
don't have bun pre-installed on self-hosted macOS runners.
Add oven-sh/setup-bun@v2 before the Rust setup step.