Notechat is a desktop application that enables you to interact with your Apple Notes through a chat interface. Built with Electron and React, it provides a seamless experience for conversing with your notes.
👉 Download NoteChat Cloud Mode
![]() Mats Funke |
![]() Arne Strickmann |
We welcome contributions from the community! Feel free to open a Pull Request.
Vite is used as the development server and build tool for the React (src/ui) part of the application. Electron handles the desktop application wrapper and native system interactions (src/electron).
.
├── README.md
├── data/ # dir for dev database file
├── dist/ # Compiled Electron files
├── dist-electron/ # Compiled Electron files
├── dist-react/ # Built React application
├── electron-builder.json # Electron build configuration
├── index.html
├── package-lock.json
├── package.json # Project metadata and dependencies
├── src
│ ├── electron/ # Electron main process code
│ │ ├── main.ts # Main electron process
│ │ ├── database
│ │ │ └── databaseService.ts # Initializes schema, handles CRUD with better-sqlite3
│ │ ├── noteProcessing
│ │ │ ├── CharChunker.ts # chunks note body by into max chunks, prefixes note title to chunk
│ │ │ └── extractAndEmbedNotes.ts # extracts, chunks, embedds and saves notes
│ │ ├── tsconfig.json # TypeScript config for electron
│ │ ├── ollamaSetup.ts
│ │ ├── pathResolver.ts
│ │ ├── preload.cts
│ │ └── util.ts
│ ├── types
│ │ └── electron.d.ts # types from electron used in frontend
│ └── ui/ # React frontend code
│ ├── App.tsx # Main React component
│ ├── assets/ # Static assets
│ ├── App.css
│ ├── index.css
│ ├── main.tsx
│ ├── constants/
│ └── components/ # reusable components
├── vite.config.ts # Vite bundler configuration
└── tsconfig.json # TypeScript config for react
# Install dependencies
npm install
# Start development environment React and Electron in development mode (hot reloading)
npm run dev
# Compiles TypeScript and builds the application
npm run build
# For a clean build macOS distribution (ARM64)
npm run clean && npm run dist:mac
Running packaged App from Terminal
run the app from /dist
# Mount (open) a DMG file
hdiutil attach dist/NoteChat-0.0.0-arm64.dmg
# Navigate to the Mounted Volume
cd /Volumes/NoteChat\ 0.0.0-arm64
# Run the Application
./NoteChat.app/Contents/MacOS/NoteChat
run the app from Applications
folder
# run the application from terminal (to view logs)
/Applications/NoteChat.app/Contents/MacOS/NoteChat
# find location of app
mdfind "kMDItemDisplayName == 'NoteChat'"
-
In the
preload.cts
, everything attached viacontextBridge
is added to thewindow
object under the keywordelectron
.electron.contextBridge.exposeInMainWorld("electron", { getStaticData: () => console.log("static"), // other methods });
In the browser's console within the app, you can execute
window.electron.getStaticData()
, and it will log "static".
Here's the updated documentation with real examples from your chat application:
Electron's IPC system enables secure communication between the main process (Node.js) and renderer process (browser).
The preload.cts
script acts as a bridge, exposing only specific functions and data to the renderer process through the contextBridge.
Basic Structure:
- Preload Script (preload.cts): Defines what the renderer can access
- Main Process (main.ts): Handles the actual functionality
- Renderer Process: Uses the exposed functions in React components
In main.ts
, we ensure the preload.cts
is run before opening the window:
const mainWindow = new BrowserWindow({
webPreferences: {
preload: getPreloadPath(),
},
});
The preload.cts
script defines the API that will be available to the renderer process. We use TypeScript interfaces to ensure type safety:
const { contextBridge, ipcRenderer } = require("electron");
// Define the API types
interface ElectronAPI {
checkOllamaStatus: () => Promise<{ running: boolean; error?: string }>;
startOllama: () => Promise<boolean>;
}
// Expose the API to the renderer process
contextBridge.exposeInMainWorld("electron", {
checkOllamaStatus: () => ipcRenderer.invoke("checkOllamaStatus"),
startOllama: () => ipcRenderer.invoke("startOllama"),
} as ElectronAPI);
In the main.ts
process, we handle the IPC calls defined in the preload script. Each handler performs its specific task and returns the result:
app.on("ready", () => {
// Handle Ollama status checks
ipcMain.handle("checkOllamaStatus", async () => {
try {
const isRunning = await checkOllamaRunning();
return { running: isRunning };
} catch (error) {
return { running: false, error: error.message };
}
});
});
In your React components, you can now safely call these methods through the window.electron
object:
function Chat() {
useEffect(() => {
const checkStatus = async () => {
const status = await window.electron.checkOllamaStatus();
setOllamaStatus(status);
};
checkStatus();
}, []);
const handleStartOllama = async () => {
const success = await window.electron.startOllama();
if (success) {
// Handle successful start
}
};
}
Define the window interface in electron.d.ts
to ensure TypeScript recognizes the electron API:
declare global {
interface Window {
electron: {
checkOllamaStatus: () => Promise<{ running: boolean; error?: string }>;
startOllama: () => Promise<boolean>;
};
}
}
You can test these IPC methods directly in the browser's DevTools console:
// Check Ollama status
await window.electron.checkOllamaStatus();
// Returns: { running: true }
// Start Ollama
await window.electron.startOllama();
// Returns: true
This implementation provides a secure and type-safe way to communicate between processes in your Electron application. The preload script acts as a security boundary, exposing only the necessary functionality to the renderer process while maintaining the isolation between Node.js and browser contexts.
The databaseService.ts
file defines a DatabaseService class that manages interactions with a SQLite database using the better-sqlite3
library. It is designed as a singleton, ensuring only one instance of the database connection is active at any time. This service handles the initialization of the database schema, ensuring necessary tables exist, and provides methods for saving notes and chunks of data. It also supports vector operations using sqlite-vec
for tasks like finding similar data entries based on vector similarity.
need to load sqlite-vec extension like this:
const extensionPath = path.join(
app.getAppPath().replace('app.asar', 'app.asar.unpacked'),
'node_modules/sqlite-vec-darwin-arm64/vec0.dylib'
);
this.vectorDb.loadExtension(extensionPath);
because node api uses meta.url() to detect path which doesn’t work for packaged electron version since it detects app.asar
as folder but its actually in app.asar.unpacked
, the above fixes: `
SqliteError: dlopen(/Users/matsfunke/dev/AppName/AppName.app/Contents/Resources/app.asar/node_modules/sqlite-vec-darwin-arm64/vec0.dylib.dylib, 0x000A): tried: '/Users/matsfunke/dev/AppName/AppName.app/Contents/Resources/app.asar/node_modules/sqlite-vec-darwin-arm64/vec0.dylib.dylib' (errno=20)
If you encounter the following error:
Failed to initialize application: Error: The module '.../better_sqlite3.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 115.
This version of Node.js requires NODE_MODULE_VERSION 123.
Follow these steps to resolve it:
# Remove existing node_modules and package-lock.json
rm -rf node_modules package-lock.json
# Clear npm cache
npm cache clean --force
# Reinstall dependencies
npm install
# Rebuild native modules for Electron
npx electron-rebuild
# Start the application
npm run dev