contextRef (useRef) was initialized at mount time and synced via useEffect,
which runs after paint. If contextPrompt was empty at mount (tab content
not yet loaded), sendMessage could read stale/empty context during the
window between paint and effect. Removing the ref and reading contextPrompt
directly in the useCallback closure eliminates the race entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When viewing a Type note (e.g. "Project"), the Properties panel now shows
an Instances section listing all notes of that type, sorted by modified_at
descending. Trashed instances are excluded, archived instances are dimmed.
Display capped at 50 with count badge for large collections.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Flush unsaved editor content to disk before any trash or archive
operation (both single-note and bulk) so body edits are never silently
dropped when only frontmatter is updated.
- Add flushEditorContent utility that checks pending content ref, then
falls back to comparing tab content with last-saved state
- Add onBeforeAction callback to useEntryActions, called before
handleTrashNote and handleArchiveNote
- Wire flushBeforeAction in App.tsx using refs for stable closures
- Add error handling in useBulkActions so one failed save doesn't
block remaining notes
- Extract findOrCreateType helper to reduce useEntryActions complexity
- Export persistContent from useSaveNote for reuse
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The body field in buildContextSnapshot was passing the full raw file
content (including YAML frontmatter delimiters) instead of just the
body text. When handleEditorChange reconstructed tab content with empty
blocksToMarkdownLossy output, the body became frontmatter-only — causing
the AI to report "has frontmatter but no body content."
Three changes:
1. Strip frontmatter from body using splitFrontmatter before setting the
body field (frontmatter is already a separate parsed field)
2. Add wordCount to the context snapshot so the AI can detect when body
is stale vs genuinely empty
3. Instruct the AI to call get_note MCP tool when body is empty but
wordCount > 0, providing a safety net for any content staleness
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handleContentChange now syncs content to tab state on every editor
change (not just on Cmd+S save). This ensures the AI panel's context
snapshot always contains the current editor body, fixing the bug where
the AI reported empty note bodies for unsaved edits.
Root cause: pendingContentRef buffered content for save but never
updated notes.tabs[].content, so activeTab?.content (used by the AI
context builder) always reflected the last-saved disk content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: CSS zoom on document.documentElement caused stale CodeMirror
measurements. Two issues:
1. Race condition: zoom was applied in useEffect (parent), but CodeMirror
was created in useEffect (child) — child effects run first, so CM
measured at zoom=1 before zoom was actually applied.
2. No re-measure on zoom change: CSS zoom changes don't trigger
ResizeObserver on descendant elements, so CodeMirror never updated
its cached scaleX/scaleY, line heights, or character widths.
Fix:
- Apply zoom synchronously during useState init (before child effects)
- Dispatch 'laputa-zoom-change' event when zoom changes
- Listen for this event in useCodeMirror and call view.requestMeasure()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In Tauri mode, allContent is {} (empty) until a note is explicitly
saved. The previous mergeTabContent fix enriched allContent with tab
content, but the indirection was fragile. This fix passes the active
tab's content directly to buildContextSnapshot as activeNoteContent,
which takes priority over allContent[path]. This ensures the AI always
receives the note body regardless of allContent state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each CLI invocation is a fresh subprocess — --resume depends on session
persistence that doesn't reliably work with -p mode. Switch to prompt-
embedded history: prior exchanges are formatted into each request using
formatMessageWithHistory + trimHistory (already existed as dead code).
- Remove sessionIdRef and --resume session tracking
- Always send system prompt (each request is stateless)
- Split sendMessage into doSend(text, history) to handle retry correctly
- Retry now passes correct history (excludes the retried exchange)
- Tests verify history accumulation, clear, retry, and system prompt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
allContent is empty ({}) in Tauri mode because loadVaultData never
populates it — note content only enters allContent on explicit save.
mergeTabContent() enriches allContent with open tab content so the AI
context snapshot always includes the active note's body.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
detectFileOperation silently failed when tool input was undefined (timing
issues with NDJSON event ordering). Added onVaultChanged callback as fallback:
when Write/Edit/Bash tools complete but specific file can't be determined,
vault.reloadVault() is triggered. Also added safety net in onDone handler.
Threaded onVaultChanged through AiPanel → EditorRightPanel → Editor → App.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: buildContextSnapshot (the system prompt used when a note is
active — the common case) did NOT instruct the AI to use [[wikilinks]].
Only buildAgentSystemPrompt (the fallback when no context) had it.
So the AI almost never produced clickable wikilinks.
Fix: Add the [[Note Title]] wikilink instruction to
buildContextSnapshot's preamble.
The click handler chain was already correct (verified with new
integration tests): MarkdownContent → AiMessage → AiPanel →
notes.handleNavigateWikilink → findWikilinkTarget → handleSelectNote.
Tests added:
- Unit: buildContextSnapshot includes wikilink instruction
- Integration: clicking wikilinks in AiPanel calls onOpenNote
- Playwright: wikilink renders, click opens note in tab
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous approach embedded conversation history as formatted text
in the prompt (<conversation_history> tags). The claude CLI's -p flag
treats this as a single user turn, losing turn boundaries — so the
model had no real multi-turn context.
Switch to using --resume with the session_id returned by the CLI's
init event. This lets the CLI manage conversation state natively:
- First message starts a new session (with system prompt)
- Subsequent messages resume via --resume (no system prompt needed)
- Clear and retry reset the session_id for a fresh start
Extract makeStreamCallbacks() to keep useAIChat complexity below
CodeScene threshold.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
The command was gated on `mcpStatus !== 'checking'` which meant it was
hidden during the initial async status check. Changed enabled to always
be true so users can find and run the command immediately on app start.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Structural changes (sidebar visibility, label, order, template, icon/color)
now auto-create missing Type entries and call onFrontmatterPersisted to
refresh git status, so changes appear in git diff without user intervention.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AiPanel.handleNavigateWikilink was double-resolving: it resolved the
wikilink target to an entry path, then passed the path to onOpenNote
which is notes.handleNavigateWikilink (expects a title-based target).
The path didn't match any entry's title, so navigation silently failed.
Fix: pass the wikilink target string directly to onOpenNote, letting
the parent's handleNavigateWikilink do the resolution.
Also enhanced the Playwright smoke test to verify click → tab opens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
- 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>
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
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)
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>
When searching in Cmd+K, groups are now ordered by their highest-scoring
match instead of the fixed section order. Empty query preserves the
default section ordering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>