Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 36 additions & 122 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,144 +1,58 @@
name: Release Build

name: Release
on:
release:
types: [published]

permissions:
contents: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
push:
tags:
- 'v*'
workflow_dispatch:

jobs:
build:
release:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest
os: macos
- platform: ubuntu-latest
os: linux
- platform: windows-latest
os: windows
- platform: 'macos-latest'
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest'
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
- platform: 'windows-latest'
args: ''

runs-on: ${{ matrix.platform }}

steps:
# Checkout full history (needed for proper tagging/version checks)
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20

# Ensure tag matches package.json version
- name: Ensure tag matches package version
shell: bash
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
TAG=${GITHUB_REF#refs/tags/v}
VERSION=$(node -p "require('./package.json').version")
if [ "$TAG" != "$VERSION" ]; then
echo "Tag ($TAG) does not match package.json version ($VERSION)"
exit 1
fi

# Setup Bun
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf

# Cache Bun dependencies
- name: Cache Bun dependencies
uses: actions/cache@v5
- name: setup node
uses: actions/setup-node@v4
with:
path: |
~/.bun/install/cache
bun.lockb
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb') }}

# Setup Tiptap Pro registry auth
- name: Configure TipTap Pro auth
shell: bash
run: |
echo "@tiptap-pro:registry=https://registry.tiptap.dev/" > .npmrc
echo "//registry.tiptap.dev/:_authToken=${NODE_AUTH_TOKEN}" >> .npmrc
env:
NODE_AUTH_TOKEN: ${{ secrets.TIPTAP_TOKEN }}

# Install dependencies
- name: Install dependencies
run: bun install --frozen-lockfile

# Build Next.js (static export to out/)
- name: Build frontend
run: |
bun run build
node-version: 20
cache: 'npm'

# Setup Rust
- name: Setup Rust
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.os == 'macos' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

# Cache Rust dependencies
- name: Cache Rust dependencies
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
src-tauri/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-

# Linux system deps
- name: Install Linux dependencies
if: matrix.os == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev

# Windows long path fix
- name: Enable long paths (Windows)
if: matrix.os == 'windows'
run: git config --system core.longpaths true

# Build and upload release assets
- name: Build and upload release assets
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
releaseId: ${{ github.event.release.id }}
args: ${{ matrix.os == 'macos' && '--target universal-apple-darwin' || '' }}
includeUpdaterJson: false
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

- name: Generate release checksums
run: node scripts/generate-release-checksums.mjs --root src-tauri/target/release/bundle --out checksums-${{ matrix.os }}.sha256
- name: install frontend dependencies
run: npm ci

- name: Upload checksum file
uses: actions/upload-release-asset@v1
- uses: tauri-apps/tauri-action@v0
Comment on lines +46 to +49
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.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./checksums-${{ matrix.os }}.sha256
asset_name: checksums-${{ matrix.os }}.sha256
asset_content_type: text/plain
tagName: app-v__VERSION__
releaseName: 'Plum Notes v__VERSION__'
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.

Spelling/branding mismatch: the app is referred to as "Pulm Notes" elsewhere (e.g., tauri.conf.json productName), but the release name uses "Plum Notes". Align naming to avoid publishing releases under the wrong name.

Suggested change
releaseName: 'Plum Notes v__VERSION__'
releaseName: 'Pulm Notes v__VERSION__'

Copilot uses AI. Check for mistakes.
releaseBody: 'See the assets to download this version and install.'
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
19 changes: 19 additions & 0 deletions RELEASE_NOTES_v0.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Plum Notes v0.1.0 🚀

The inaugural release of Plum Notes—a blazing-fast, local-first second brain for developers and creators.

This v0.1.0 release establishes the complete core loop of the application: frictionless capture, instant retrieval, deep linking, and triage.

## Highlights
* **Frictionless Quick Capture:** Hit `Cmd/Ctrl + Shift + Space` anywhere on your OS to summon a floating scratchpad. Sub-100ms time to write.
* **Bulletproof Local Storage:** Your data is yours. Everything is saved instantly to a local SQLite database via Tauri FS. Offline by default, private by design.
* **Command Palette:** Keyboard-first navigation (`Cmd+K`) powered by Ariakit, with deep fuzzy-search across all note contents and blocks.
* **Knowledge Graph & Backlinks:** Connect your thoughts using `[[wiki-style]]` linking. Visualize your second brain with an interactive, highly-optimized 2D force graph.
* **Inbox Triage:** Say goodbye to the black hole. Uncategorized captures land in a dedicated Inbox where you can Promote (`P`), Snooze (`S`), or Discard (`D`) using pure keyboard shortcuts.
* **Tagging & Filtering:** Inline `#tag` support in the editor with a multi-select AND-filter sidebar to instantly slice your library.

## Installation
Download the appropriate binary for your system from the assets below:
* **macOS:** `.dmg` (Apple Silicon & Intel supported)
* **Windows:** `.msi`
* **Linux:** `.AppImage` or `.deb`
99 changes: 99 additions & 0 deletions app/components/BacklinksSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use client';

import React from 'react';
import { Note } from '@/app/types';
import { FileText, Link as LinkIcon } from 'lucide-react';

interface BacklinksSidebarProps {
notes: Note[];
currentNoteId: string | null;
onSelectNote: (noteId: string) => void;
}

export const BacklinksSidebar: React.FC<BacklinksSidebarProps> = ({
notes,
currentNoteId,
onSelectNote
}) => {
if (!currentNoteId) return null;

const currentNote = notes.find(n => n.id === currentNoteId);
if (!currentNote) return null;

// Find all notes that mention the current note
const backlinks = notes.filter(note => {
if (note.id === currentNoteId || note.isDeleted) return false;

// 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;
});
}

Comment on lines +27 to +51
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.
return false;
});
});

return (
<div className="w-72 h-full bg-stone-50 flex flex-col border-l border-stone-200">
<div className="p-6 pb-4 border-b border-stone-200">
<div className="flex items-center gap-2">
<LinkIcon size={16} className="text-stone-400" />
<h2 className="text-sm font-medium text-stone-500 tracking-wide">
Backlinks
</h2>
</div>
</div>

<div className="flex-1 overflow-y-auto p-4 space-y-3">
{backlinks.length > 0 ? (
backlinks.map(note => (
<div
key={note.id}
onClick={() => onSelectNote(note.id)}
className="p-3 bg-white border border-stone-200 rounded-lg shadow-sm cursor-pointer hover:border-stone-300 hover:shadow transition-all group"
>
<div className="flex items-center gap-2 mb-1.5">
<FileText size={14} className="text-stone-400 group-hover:text-stone-600 transition-colors" />
<h3 className="text-sm font-medium text-stone-700 truncate">
{note.title}
</h3>
</div>
<p className="text-xs text-stone-500 line-clamp-2 leading-relaxed">
{/* Extract a snippet around the mention, or just show the start of the note */}
{note.blocks.find(b => b.type === 'text')?.content || 'Empty note...'}
</p>
</div>
))
) : (
<div className="text-center py-8 px-4">
<div className="w-10 h-10 rounded-full bg-stone-100 flex items-center justify-center mx-auto mb-3">
<LinkIcon size={18} className="text-stone-300" />
</div>
<p className="text-sm text-stone-500">No notes link to this one yet.</p>
<p className="text-xs text-stone-400 mt-1">Type [[ to link notes together.</p>
</div>
)}
</div>
</div>
);
};
Loading
Loading