Vulpecula Loom is a desktop application that combines the power of modern AI language models with seamless Obsidian vault integration. Built with Electron, Vue 3, and TypeScript, it offers a polished and efficient interface for AI-assisted writing and research.
- Support for multiple AI models through OpenRouter
- Claude 3 (Opus & Sonnet)
- GPT-4
- GPT-3.5 Turbo
- Real-time token counting and cost tracking
- Markdown rendering with syntax highlighting
- Chat history with persistent storage
- Export conversations to Markdown
Share your knowledge with AI by referencing your notes. Simply type @
in any message to seamlessly include content from your Obsidian vault:
-
Quick Note References
Hey AI, can you help me understand @quantum-computing-basics? Also, how does it relate to @quantum-entanglement?
- Type
@
anywhere in your message - Search your vault in real-time
- Select files from the popup
- Multiple files can be referenced in one message
- Type
-
How It Works
- When you include a note with
@
, the entire note's content is sent to the AI - The AI can then reference and analyze your notes
- Example:
User: Can you compare the concepts in @classical-computing with @quantum-computing? Assistant: Looking at your notes, I can see several key differences... [Assistant can now reference the full content of both notes]
- When you include a note with
-
File Search Features
- Real-time search as you type after
@
- Instant results (debounced 150ms)
- Shows file previews
- Matches titles and content
- Keyboard navigation (↑↓ to select, Enter to choose)
- Results sorted by relevance:
- Title matches
- Content matches
- Recently modified
- Real-time search as you type after
-
Technical Implementation
- Files included via
includedFiles
array in messages - Each included file contains:
{ title: string; // File title path: string; // Full path to file content: string; // Complete file content }
- Content is cached for performance
- Files monitored for changes
- Vault path stored securely
- Files included via
-
Security & Performance
- Files only accessible within your vault
- Content sanitized before display
- No external file access
- Efficient caching system
- Background indexing
- Incremental updates
-
File Inclusion
- Files can be included in messages using
@
mentions - When a file is included, its entire content is added to the message
- Included files are tracked in the message's
includedFiles
array - Each included file contains:
{ title: string; // File title path: string; // Full path to file content: string; // Complete file content }
- Files can be included in messages using
-
File Search
- Real-time search as you type
@
- Debounced queries (150ms)
- Results limited to 10 files
- Results sorted by relevance
- Cache management for performance
- Real-time search as you type
-
Security
- Files are only accessible within the vault
- Content is sanitized before display
- No external file access allowed
- Vault path stored securely in electron-store
-
Performance
- File content cached in memory
- Incremental search updates
- Debounced search queries
- Background file indexing
- Dark/Light mode support
- Native system integration
- Custom titlebar with gradient animation
- Responsive design with smooth transitions
- Keyboard shortcuts for common actions
All inter-process communication (IPC) is centralized in electron/ipc/
with the following structure:
electron/ipc/
├── index.ts # Central IPC setup and system-level handlers
├── obsidian.ts # Obsidian integration handlers
└── store.ts # Electron store handlers
All IPC channels are fully typed using TypeScript interfaces in src/types.ts
under the IpcChannels
interface.
This centralized type system provides:
- Auto-completion for channel names
- Parameter type checking
- Return type inference
- Compile-time validation
- Single source of truth for all IPC types
Example:
// In src/types.ts
export interface IpcChannels {
"open-external": (url: string) => Promise<void>;
"store-get": <K extends keyof StoreSchema>(key: K) => Promise<StoreSchema[K]>;
// ... more channel definitions
}
- Add the type definition to
src/types.ts
under theIpcChannels
interface - Create a new handler in the appropriate domain file in
electron/ipc/
- Register the handler in
electron/ipc/index.ts
- Update this documentation
- Node.js (v18 or higher)
- npm or yarn
- An OpenRouter API key
- Obsidian vault (optional)
git clone https://github.com/yourusername/vulpecula-loom.git
cd vulpecula-loom
yarn install
yarn build
The built application will be available in the release/{version}/
directory:
electron-vue-vite.app
- The macOS application bundleelectron-vue-vite-{version}-arm64.dmg
- The disk image installer for Apple Siliconelectron-vue-vite-{version}-x64.dmg
- The disk image installer for Intel Macs
Note: To build a signed application for distribution, you'll need an Apple Developer account and appropriate certificates. See Apple's documentation for more details on app notarization.
ALL types MUST live in src/types.ts
. This is not just a convention - it's a requirement.
- Single source of truth
- No duplicate definitions
- Clear type hierarchies
- Easy to find and modify
- Better TypeScript tooling support
- Prevents circular dependencies
- Maintains type consistency
- OpenRouter Types (API models, responses)
- Chat Types (messages, history)
- Store Types (electron store schema)
- Electron IPC Types (channel definitions)
- UI Types (preferences, settings)
- Animation Types (configuration)
- ALL types MUST be in
src/types.ts
- NO
.d.ts
files allowed (except for third-party type declarations) - NO interface/type definitions in component files
- NO scattered type definitions across the codebase
- ALL types MUST be properly categorized and documented
- ALL types MUST be exported
❌ ILLEGAL:
- Types in component files
- Scattered
.d.ts
files - Duplicate type definitions
- Undocumented types
- Types without proper categorization
✅ LEGAL:
- All types in
src/types.ts
- Clear type categorization
- Proper exports
- Well-documented types
- Single source of truth
While we maintain strict type centralization, we allow any
or more permissive types when:
- Dealing with third-party libraries
- Prototyping features
- Handling complex DOM elements
- Working with dynamic data
Remember: Type centralization is about organization, not restriction.
The application uses a layered composable architecture:
-
useAIChat
- The main chat composable that handles all chat functionality. This is the primary interface that components should use for chat operations. It provides:- Message handling
- Token tracking
- Cost calculation
- Chat persistence
- Streaming responses
- History management
-
useOpenRouter
- Low-level OpenRouter API integration. Should not be used directly by components. Instead, useuseAIChat
which provides a higher-level interface and proper state management. -
useSupabase
- Handles chat persistence and history management -
useObsidianFiles
- Manages Obsidian vault integration -
useTheme
- Handles theme switching and persistence
ALWAYS use useAIChat.ts
for chat functionality!
The chat system is designed around the useAIChat
composable, which handles:
- Message sending and receiving
- Chat history management
- Token tracking
- Cost calculation
- Model selection
- Temperature settings
- Chat statistics
❌ DO NOT try to implement chat functionality by directly using useOpenRouter
or other lower-level composables. This will:
- Break the chat history system
- Cause message tracking issues
- Prevent proper token/cost tracking
- Summon dragons that will devour everyone
useOpenRouter
should only be used for:
- API key management
- Model availability checking
- Model information
The application uses electron-store
for persistent storage. The store is exposed to the renderer process through a secure preload script.
-
Store Schema
- All store keys are defined in
src/types.ts
- Keys follow a consistent naming pattern (e.g.,
api-key
,theme
) - Values are strongly typed
- All store keys are defined in
-
Access Pattern
- Always use the
useStore
composable - Never access
window.electron.store
directly - All store operations are async
- Always use the
-
Key Categories
- API Keys (
api-key
) - UI Preferences (
theme
,show-progress-bar
) - Model Settings (
enabled-model-ids
,pinned-models
) - Application State (
remember-window-state
) - Integration Settings (
obsidian-vault-path
)
- API Keys (
-
Best Practices
- Use typed keys from
StoreSchema
- Handle errors gracefully
- Provide default values
- Log store operations in development
- Clear store data responsibly
- Use typed keys from
The application follows a strict type centralization pattern:
-
Central Type Repository
- ALL types MUST live in
src/types.ts
- NO scattered
.d.ts
files (except for third-party declarations) - NO interface/type definitions in component files
- ALL types MUST be properly categorized and documented
- ALL types MUST live in
-
Type Categories
- OpenRouter Types (API models, responses)
- Chat Types (messages, history)
- Store Types (electron store schema)
- Electron IPC Types (channel definitions)
- UI Types (preferences, settings)
- Animation Types (configuration)
- Obsidian Types (file handling, search)
-
Naming Conventions
- Interface names are PascalCase and descriptive (e.g.,
ObsidianFile
,ChatMessage
) - Options/Config interfaces end with respective suffix (e.g.,
ObsidianSearchOptions
,AnimationConfig
) - Props interfaces end with
Props
(e.g.,ChatInputProps
) - Return type interfaces end with
Return
(e.g.,UseSupabaseReturn
)
- Interface names are PascalCase and descriptive (e.g.,
The application uses a layered composable architecture:
-
Core Composables
useAIChat
- Primary chat interfaceuseOpenRouter
- Low-level API integrationuseSupabase
- Chat persistenceuseObsidianFiles
- Vault integrationuseTheme
- Theme management
-
Composable Rules
- Each composable has a single responsibility
- Composables can depend on other composables
- State management handled through Vue's reactivity system
- Error handling at every layer
- Type-safe return values
-
Obsidian Integration
useObsidianFiles
manages all Obsidian interactions- File search with debounced queries
- Cache management for performance
- Type-safe IPC communication
- Error handling and state management
-
Channel Organization
- All channels defined in
IpcChannels
interface - Strongly typed parameters and return values
- Channels grouped by domain:
- Store operations (
store-get
,store-set
) - File operations (
search-obsidian-files
,get-obsidian-file-content
) - System operations (
open-external
)
- Store operations (
- All channels defined in
-
Security Model
- Context isolation enabled
- Node integration disabled
- Explicit channel allowlist
- Type-safe preload script
-
Error Handling
- All IPC calls wrapped in try/catch
- Errors propagated to UI
- Fallback values defined
- Error states tracked in composables
-
Store Organization
- Electron store for persistent data
- Vue refs for component state
- Computed properties for derived state
- Watchers for side effects
-
Data Flow
- Props down, events up
- State centralized in composables
- IPC communication abstracted
- Type-safe store operations
src/
├── components/ # Vue components
├── composables/ # Vue composables
├── electron/ # Electron main process
│ └── ipc/ # IPC handlers
├── lib/ # Shared utilities
└── types.ts # Central type definitions
-
Type Safety
- Use TypeScript's strict mode
- No
any
types unless absolutely necessary - Proper type imports from central
types.ts
- Type-safe IPC communication
-
Error Handling
- Graceful degradation
- User-friendly error messages
- Proper error propagation
- Error state management
-
Performance
- Debounced search queries
- File caching
- Lazy loading where appropriate
- Efficient IPC communication
-
Code Style
- Clear naming conventions
- Consistent file structure
- Proper documentation
- Single responsibility principle