Skip to content

Core Loop & Hardening & etc#51

Open
yethikrishna wants to merge 14 commits intoLuxion-Labs:mainfrom
yethikrishna:main
Open

Core Loop & Hardening & etc#51
yethikrishna wants to merge 14 commits intoLuxion-Labs:mainfrom
yethikrishna:main

Conversation

@yethikrishna
Copy link
Copy Markdown

Description

This PR implements the entire "Block A", "Block B", and "Block D" functionality from the v0.1 roadmap, transforming Pulm Notes from a prototype into a fully functional, local-first second brain.

Core Loop & Hardening (v0.1)

  • Frictionless Quick Capture: Added a global OS hotkey (Cmd/Ctrl + Shift + Space) that summons a floating Tauri window for sub-100ms capture.
  • Bulletproof Local Storage: Wired @tauri-apps/plugin-fs to save/load TipTap documents directly to the local SQLite database, preserving local-first guarantees.
  • Command Palette & Search: Integrated @ariakit/react for a keyboard-first Cmd+K palette, featuring deep full-text fuzzy search inside note blocks and list items.
  • DB Migrations: Built a safe SQLite migration runner in db.rs to handle future schema bumps without crashing.
  • CI/CD: Configured GitHub Actions release workflow for deterministic cross-platform Tauri builds (macOS, Windows, Linux).

Second Brain & Growth (Post-v0.1)

  • Note Linking & Backlinks: Added [[wiki-style]] note linking. Built a new BacklinksSidebar that automatically scans and surfaces bidirectional links when reading a note.
  • Knowledge Graph: Integrated react-force-graph-2d to build an interactive, zoomable network graph visualizing connections between notes. Highly optimized (O(N)) to prevent quadratic scanning bottlenecks.
  • Inbox Triage Flow: Added an InboxView to solve the "black hole" problem. Users can now rapidly triage quick captures using keyboard shortcuts: P (Promote to category), S (Snooze/Pin), and D (Discard).

Related Issue

NIL (NOVEL)

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring
  • Build/CI configuration change

Platform

  • Web App
  • Desktop App (Windows)
  • Desktop App (macOS)
  • Desktop App (Linux)

Testing

Test Environment

  • OS: macOS / Ubuntu Linux
  • Browser (if web): Chrome / Safari
  • Node.js version: 20.x
  • Bun version: 1.x

Test Steps

  1. Run bun run tauri:dev.
  2. Press Cmd+Shift+Space to open Quick Capture, type a note, and save.
  3. Open the main app window, click the Inbox tab in the sidebar. Verify the new note appears.
  4. Press P to promote the note to a category.
  5. In the note editor, type [[ and select another note to create a link.
  6. Open the linked note and verify the Backlinks sidebar appears with the correct reference.
  7. Click the Graph View icon in the sidebar and verify the nodes and edges render correctly.
  8. Press Cmd+K and search for text deep inside a note block to verify full-text search.

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published
  • I have checked my code and corrected any misspellings
  • I have signed off my commits (DCO)

Breaking Changes

  • This PR introduces breaking changes
  • Migration guide has been provided

Additional Notes

  • Graph construction was specifically optimized to avoid O(N^2) scanning via a titleToIdMap, ensuring the app remains responsive at 1000+ notes.
  • The SQLite migration runner tracks a migration_run flag to safely handle older database versions without falsely bumping the user_version PRAGMA.

Copilot AI review requested due to automatic review settings April 16, 2026 17:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR expands Pulm Notes from a prototype into a more complete local-first desktop experience by adding a global quick-capture flow, richer navigation modes (Inbox/Graph), backlinks, deeper command-palette search, and a revamped release pipeline.

Changes:

  • Add a global OS shortcut that toggles a hidden “quick-capture” Tauri window and emits an event to refresh notes in the main window.
  • Introduce new UI views: Inbox triage, knowledge graph visualization, and backlinks sidebar; enhance command palette search to include note block content.
  • Update DB schema-version handling to support future migrations and replace the release workflow for cross-platform Tauri builds.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
src-tauri/tauri.conf.json Enables macOS private API usage and defines the new quick-capture window.
src-tauri/src/main.rs Registers global shortcut plugin and adds a command/event for quick-capture note refresh.
src-tauri/src/db.rs Adds a migration runner scaffold and changes behavior for version mismatches.
src-tauri/Info.plist Adds macOS plist configuration (agent app behavior).
src-tauri/Cargo.toml Adds tauri-plugin-global-shortcut.
app/quick-capture/page.tsx New quick-capture editor route that saves a note and triggers refresh.
app/page.tsx Listens for the “note-saved” event, adds Inbox/Graph/Backlinks rendering, and Inbox actions.
app/components/Sidebar.tsx Adds Inbox and Graph navigation entries and an inbox badge.
app/components/InboxView.tsx New inbox triage UI with keyboard shortcuts.
app/components/GraphView.tsx New force-graph view that builds a note-link graph via mentions/wiki-links.
app/components/BacklinksSidebar.tsx New backlinks panel that scans notes for links to the current note.
app/components/CommandPalette.tsx Moves the palette to Ariakit combobox/dialog and expands search into block content.
app/types.ts Extends ViewMode to include graph and inbox.
package.json Removes TipTap Pro AI deps and adds react-force-graph-2d.
roadmap.md Adds a v0.1 roadmap document.
.github/workflows/release.yml Replaces the release workflow to build Tauri bundles on tag pushes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +571 to +575
mode === 'inbox' ? 'text-indigo-400' : ''
}`} />
{mode === 'inbox' && inboxCount > 0 && (
<span className="absolute -top-1.5 -right-1.5 text-[9px] font-bold
bg-indigo-500 text-white rounded-full w-3.5 h-3.5
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

inboxCount is referenced here, but it isn't pulled from props in the Sidebar component's parameter destructuring. This will fail typechecking and can throw at runtime. Destructure inboxCount from props (or reference it via props.inboxCount).

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +66
categoryStore.loadCategories().then(categories => {
const uncategorized = categories.find(c => c.name === 'Uncategorized');
const categoryId = uncategorized ? uncategorized.id : '';

const newNote: Note = {
id: `note-${generateId()}`,
title,
blocks,
categoryId,
createdAt: new Date(),
updatedAt: new Date(),
};
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

If the "Uncategorized" category doesn't exist yet, this sets categoryId to an empty string. That will prevent the note from showing up in the Inbox logic (which filters by the Uncategorized category id) and may break other code that assumes categoryId is non-empty. Use the same fallback strategy as the main app (find or create an "Uncategorized" category and use its id) or fall back to a sentinel id consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +51
// Search blocks for mentions of currentNoteId
return note.blocks.some(block => {
// Check block mentions array if it exists
if (block.mentions && block.mentions.some(m => m.noteId === currentNoteId)) {
return true;
}

// Fallback: check raw text content for the wiki-link pattern [[title]]
if (block.content && block.content.includes(`[[${currentNote.title}]]`)) {
return true;
}

// Check list items
if (block.items) {
return block.items.some(item => {
if (item.mentions && item.mentions.some(m => m.noteId === currentNoteId)) {
return true;
}
if (item.content && item.content.includes(`[[${currentNote.title}]]`)) {
return true;
}
return false;
});
}

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

Block (from @/app/types) doesn't define items, so this block.items access will fail typechecking. Also, list entries appear to be stored as individual list blocks, so backlinks should be derivable by scanning each block's mentions/content without nested items.

Suggested change
// Search blocks for mentions of currentNoteId
return note.blocks.some(block => {
// Check block mentions array if it exists
if (block.mentions && block.mentions.some(m => m.noteId === currentNoteId)) {
return true;
}
// Fallback: check raw text content for the wiki-link pattern [[title]]
if (block.content && block.content.includes(`[[${currentNote.title}]]`)) {
return true;
}
// Check list items
if (block.items) {
return block.items.some(item => {
if (item.mentions && item.mentions.some(m => m.noteId === currentNoteId)) {
return true;
}
if (item.content && item.content.includes(`[[${currentNote.title}]]`)) {
return true;
}
return false;
});
}
// Search each block for mentions of currentNoteId
return note.blocks.some(block => {
// Check block mentions array if it exists
if (block.mentions && block.mentions.some(m => m.noteId === currentNoteId)) {
return true;
}
// Fallback: check raw text content for the wiki-link pattern [[title]]
if (block.content && block.content.includes(`[[${currentNote.title}]]`)) {
return true;
}

Copilot uses AI. Check for mistakes.
Comment thread src-tauri/src/main.rs
use std::path::PathBuf;
use std::sync::Arc;
use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, Modifiers, ShortcutState};
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

Code and Modifiers are imported but never used. This will generate Rust warnings (and can fail builds if warnings are denied). Remove the unused imports or use them explicitly.

Suggested change
use tauri_plugin_global_shortcut::{Code, Modifiers, ShortcutState};
use tauri_plugin_global_shortcut::ShortcutState;

Copilot uses AI. Check for mistakes.
Comment on lines +550 to 552
{(['home', 'inbox', 'recent', 'pins', 'graph'] as ViewMode[]).map((mode) => {
const Icon = mode === 'graph' ? require('lucide-react').Network : mode === 'inbox' ? Inbox : getViewModeIcon(mode);
return (
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

Using require('lucide-react').Network inside the render path is likely to break ESM/tree-shaking and can cause bundling issues in Next.js. Import the Network icon at the top (like the other lucide icons) and reference it directly.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +49
- name: install frontend dependencies
run: npm ci

- name: Upload checksum file
uses: actions/upload-release-asset@v1
- uses: tauri-apps/tauri-action@v0
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

This workflow installs dependencies with npm ci, but src-tauri/tauri.conf.json uses beforeBuildCommand: "bun run build". Unless Bun is installed on the runner, the Tauri build step will fail when it executes that command. Either install Bun here (like the CI workflow does) or switch the Tauri beforeBuildCommand to an npm-based command.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +109
targetIds.forEach(targetId => {
if (validNodeIds.has(targetId) && targetId !== note.id) {
links.push({ source: note.id, target: targetId });

// Bump size of target node to show importance
const targetNode = nodes.find(n => n.id === targetId);
if (targetNode) {
targetNode.val += 0.5;
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

nodes.find(...) runs for every edge, making graph construction potentially O(E·N) in the worst case. For large note sets this can become noticeably slow. Keep a Map<id, node> (or id→index map) so you can bump val in O(1) per edge.

Copilot uses AI. Check for mistakes.
Comment thread src-tauri/Info.plist
Comment on lines +5 to +6
<key>LSUIElement</key>
<true/>
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

Setting LSUIElement to true turns the macOS app into an agent app (no Dock icon / menu bar), which can make the main window harder to discover/restore and changes expected UX. If the goal is only to support Quick Capture, consider whether this should be removed or made conditional (e.g., only for a helper app) so the primary app remains visible in the Dock.

Suggested change
<key>LSUIElement</key>
<true/>

Copilot uses AI. Check for mistakes.
Comment thread app/page.tsx
let unlistenFn: (() => void) | undefined;

// Only use Tauri APIs when running in the Tauri desktop environment
if (typeof window !== 'undefined' && window.__TAURI__) {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

window.__TAURI__ isn't part of the standard Window type, so this will fail TypeScript under strict settings unless you've augmented the global types. Use the existing isTauriApp() helper (or ('__TAURI__' in window) with a cast) to avoid type errors and keep the environment check consistent.

Suggested change
if (typeof window !== 'undefined' && window.__TAURI__) {
if (typeof window !== 'undefined' && '__TAURI__' in window) {

Copilot uses AI. Check for mistakes.
Comment thread app/page.tsx
const handleSnooze = (noteId: string) => {
setNotes(notes.map((n) =>
n.id === noteId
? { ...n, isPinned: true, updatedAt: new Date() }
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

handleSnooze always sets isPinned: true, but the Inbox UI presents this as a toggle ("Unsnooze") and users won't be able to un-snooze once pinned. Either toggle the existing value (e.g., isPinned: !n.isPinned) or adjust the UI/text to match one-way pinning.

Suggested change
? { ...n, isPinned: true, updatedAt: new Date() }
? { ...n, isPinned: !n.isPinned, updatedAt: new Date() }

Copilot uses AI. Check for mistakes.
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