# Chat vs Create Modes Source: https://docs.glyphformac.com/ai/chat-modes Understand the difference between chat and create modes in Glyph AI Glyph AI supports two conversation modes: **Chat** and **Create**. Each mode offers different capabilities and use cases. ## Mode Overview | Feature | Chat Mode | Create Mode | | ----------------- | ---------------- | ---------------------- | | Conversation | ✅ Yes | ✅ Yes | | File reading | ❌ No | ✅ Yes (`read_file`) | | Note search | ❌ No | ✅ Yes (`search_notes`) | | Directory listing | ❌ No | ✅ Yes (`list_dir`) | | Tool timeline | ❌ No | ✅ Yes | | Response speed | ⚡ Faster | 🔧 May be slower | | Best for | Discussion, Q\&A | Research, analysis | ## Chat Mode ### What is Chat Mode? Chat mode provides direct conversational interaction with the AI model without any tool access. ### Characteristics * **Pure conversation**: No file system access * **Faster responses**: No tool call overhead * **Simpler interactions**: Straightforward Q\&A * **Context-only**: Can only reference attached context ### When to Use Chat Mode Generate ideas without needing to reference files Have back-and-forth conversations about concepts Get fast answers without file exploration Generate content without workspace references ### Example Use Cases ``` ❓ User: What are the benefits of the Zettelkasten method? 🤖 AI: [Direct response without searching files] ❓ User: Help me brainstorm blog post topics about productivity. 🤖 AI: [Generates ideas conversationally] ❓ User: Explain the concept of atomic notes. 🤖 AI: [Provides explanation from training data] ``` ## Create Mode (Default) ### What is Create Mode? Create mode gives the AI access to workspace tools, allowing it to read files, search notes, and explore your space. ### Characteristics * **Tool access**: File reading, searching, and listing * **Autonomous exploration**: AI decides when to use tools * **Evidence-based**: Can cite specific files and content * **Tool discipline**: Guided to use minimal tool calls * **Timeline view**: See which tools were called and when ### Available Tools #### `read_file` Read the contents of a file in your space. **Parameters**: * `path` - Relative path to file (e.g., `notes/2024-01-15.md`) **Limits**: * Max file size: 512 KB * Max characters: 12,000 * UTF-8 text files only **Example**: ``` AI calls: read_file({"path": "projects/glyph/roadmap.md"}) Returns: File contents (truncated if too large) ``` #### `search_notes` Search for files matching a query using hybrid search (SQLite FTS + similarity). **Parameters**: * `query` - Search query * `limit` - Max results (default 20, max 200) **Returns**: * File paths matching the query * Ranked by relevance **Example**: ``` AI calls: search_notes({"query": "AI integration", "limit": 10}) Returns: [ {"path": "notes/ai-setup.md", "score": 0.95}, {"path": "projects/ai-features.md", "score": 0.87} ] ``` #### `list_dir` List files and directories in a path. **Parameters**: * `path` - Directory path (optional, defaults to root) * `recursive` - Include subdirectories (optional) * `depth` - Max depth for recursive listing (optional) * `limit` - Max files to return (default 1000, max 5000) **Returns**: * List of files and directories * File metadata (size, modified time) **Example**: ``` AI calls: list_dir({"path": "projects", "recursive": false}) Returns: [ {"name": "glyph", "type": "dir"}, {"name": "roadmap.md", "type": "file", "size": 2048} ] ``` ### When to Use Create Mode Find and analyze information across multiple notes Summarize content from specific files or folders Answer questions by referencing your notes Analyze patterns or themes across your workspace ### Example Use Cases ``` ❓ User: What's in my roadmap file? 🤖 AI: [Calls read_file("roadmap.md") and summarizes] ❓ User: Find all notes about AI features. 🤖 AI: [Calls search_notes({query: "AI features"}) and lists results] ❓ User: What projects do I have? 🤖 AI: [Calls list_dir({path: "projects"}) and summarizes] ``` ### Tool Discipline To prevent excessive tool usage, Glyph adds this system prompt in create mode: ``` Tool discipline for this run: use the minimum number of tool calls needed. Prefer at most 1-2 search/list calls before answering. If a tool returns usable evidence, stop searching and summarize what you found with uncertainty notes rather than continuing to explore. ``` This encourages the AI to: * Use tools sparingly * Stop searching once it finds useful information * Acknowledge uncertainty instead of over-searching ## Switching Between Modes Modes are set per conversation. To change modes: 1. Start a new conversation in the AI panel 2. Mode defaults to **Create** 3. To use Chat mode, it must be selected when starting the conversation Currently, mode selection is not exposed in the UI. Create mode is the default. To use chat mode, you'll need to modify the `mode` parameter in the chat request. ## Mode Selection in Code From `src/components/ai/hooks/useRigChat.ts`: ```typescript theme={null} const { job_id: jobId } = await invoke("ai_chat_start", { request: { profile_id: profileId, messages: asAiMessages([...messagesRef.current, userMessage]), thread_id: threadId, mode: options?.body?.mode ?? "create", // Default: create context: options?.body?.context || undefined, context_manifest: options?.body?.context_manifest, audit: options?.body?.audit ?? true, }, }); ``` ## Tool Timeline In create mode, the AI panel shows a tool timeline: * 🔧 **Tool called**: Which tool, with what arguments * ⏱️ **Timestamp**: When the tool was called * ✅ **Result**: Success or error * 📊 **Payload**: Tool arguments and response This provides transparency into how the AI is exploring your workspace. ## Performance Considerations ### Chat Mode Performance * ⚡ Fast responses (no tool overhead) * 💰 Lower token usage (no tool definitions in prompt) * 🔄 Immediate streaming starts ### Create Mode Performance * 🔧 Tool call latency (AI must decide when to call tools) * 📈 Higher token usage (tool definitions + results) * ⏱️ Longer responses (tool execution time) * 🧠 More thoughtful (AI considers available tools) For simple questions, chat mode is faster. For questions requiring file access, create mode is necessary. ## Security and Safety Both modes enforce security restrictions: ### Path Safety * ✅ All paths restricted to current space * ❌ Path traversal prevented (`../` not allowed) * ❌ Hidden files blocked (files starting with `.`) * ❌ Symlinks outside space blocked ### File Limits * Max file read: 512 KB * Max characters per file: 12,000 * Max search results: 200 * Max directory list: 5,000 files ### Audit Trail Create mode logs all tool usage: * Stored in `.glyph/ai_history/.json` * Includes tool name, arguments, results, timestamps * Reviewable for compliance and debugging ## Best Practices ### Use Chat Mode For: * General knowledge questions * Creative tasks (writing, brainstorming) * Conversations that don't require file access * When you need fast responses ### Use Create Mode For: * Questions about your specific notes * Research requiring file exploration * Summarization of workspace content * When you need evidence from your files ### Combine with Context Attachment Even in chat mode, you can attach context: * Attach files or folders manually * Use `@mentions` to reference files * Context sent in system message * AI can reference without tool calls ## Troubleshooting ### Tools not being called in create mode **Possible causes**: * Model doesn't support function calling well (try GPT-4, Claude, or Gemini) * Question is answerable from training data * Tool discipline prompt limiting tool usage **Solution**: Ask more specific questions that clearly require file access. ### Tool calls failing **Possible causes**: * File doesn't exist * File is too large (>512 KB) * File is binary (not UTF-8 text) * Path contains hidden directories **Solution**: Check tool error messages in the timeline view. ### Too many tool calls **Cause**: AI is exploring extensively before answering. **Solution**: * Make questions more specific * Attach context manually to reduce exploration * Use chat mode if file access isn't needed ## Next Steps Learn to attach files and folders Configure AI profiles and models Manage multiple AI configurations Review conversation history and tool usage # Context Management Source: https://docs.glyphformac.com/ai/context-management Attach files and folders to AI conversations in Glyph Ground AI responses in your notes by attaching files and folders as context. Glyph builds a context payload from your attachments and sends it with your message. ## Overview Context attachment allows you to: * Include specific files or entire folders in conversations * Mention files using `@filename` syntax * Configure character budgets (200 - 250,000 chars) * View token estimates before sending * Store context manifest with conversation history ## How Context Works 1. **Attach**: Select files or folders to include 2. **Build**: Glyph reads and concatenates content 3. **Budget**: Content is truncated to fit character budget 4. **Estimate**: View token estimates (chars ÷ 4) 5. **Send**: Context sent in system message 6. **Store**: Manifest saved with conversation history ## Attaching Context ### Via Context Menu Click the AI icon in the sidebar or use the keyboard shortcut. Click the **Context** button or **@** icon in the AI composer. Type to filter the index: * Files: Individual notes (e.g., `roadmap.md`) * Folders: Entire directories (e.g., `projects/`) * Space: Root folder (all files) Click files or folders to attach. Selected items appear in the attached context list. View the context manifest showing: * Items attached * Character count per item * Token estimates * Truncation warnings ### Via @mentions Type `@` in your message to mention files: ``` @roadmap.md what are the key milestones? ``` Glyph automatically: 1. Detects the `@mention` 2. Searches the file index 3. Attaches the file to context 4. Removes the `@mention` from your message Multiple mentions work too: ``` Compare @design-doc.md with @implementation-notes.md ``` ## Context Index Glyph maintains an in-memory index of your space: ### Indexing * **When**: On AI panel open * **What**: All files and folders in your space * **Excludes**: Hidden files (`.gitignore`, `.env`, etc.), `node_modules` * **Limit**: Up to 20,000 files ### Structure From `src-tauri/src/ai_rig/context.rs:174`: ```rust theme={null} ai_context_index() -> AiContextIndexResponse { folders: Vec<{ path: String, label: String }>, files: Vec<{ path: String, label: String }> } ``` Folders and files are listed separately for easy filtering. ## Character Budget Control how much context to include: ### Default Budget * **Default**: 12,000 characters (\~3,000 tokens) * **Minimum**: 200 characters * **Maximum**: 250,000 characters (\~62,500 tokens) ### Adjusting Budget In the context menu, adjust the character budget slider or input: * **Lower budget**: Faster, cheaper, may truncate important content * **Higher budget**: More complete context, slower, more expensive ### How Budget is Applied 1. Files and folders are processed in order 2. Each item's content is read and formatted 3. If content exceeds remaining budget, it's truncated with `…(truncated)` suffix 4. Truncated items marked in manifest with `truncated: true` ## Context Payload Format Glyph formats attached context as: ```markdown theme={null} # Folder: projects # File: projects/roadmap.md --- # File: projects/tasks.md ``` * Folder headers mark directory attachments * File headers show relative paths * Content separated by `---` dividers * Sent in system message or user message depending on provider ## Context Manifest The manifest provides transparency into context composition. ### Manifest Structure From `src-tauri/src/ai_rig/context.rs:42`: ```typescript theme={null} interface ContextManifest { items: Array<{ kind: 'file' | 'folder', label: string, chars: number, estTokens: number, truncated: boolean }>, totalChars: number, estTokens: number } ``` ### Viewing Manifest In the AI panel: 1. Attach context 2. Click **View Manifest** or expand context section 3. See breakdown of attached items ### Manifest Storage Manifests are stored with conversation history: ```json theme={null} { "job_id": "uuid", "messages": [...], "context_manifest": { "items": [...], "totalChars": 15000, "estTokens": 3750 } } ``` This allows you to review what context was sent with past conversations. ## Token Estimation Glyph estimates tokens using: ```rust theme={null} fn estimate_tokens(chars: usize) -> usize { chars.div_ceil(4) } ``` **Formula**: `tokens ≈ characters ÷ 4` This is a rough estimate. Actual tokenization varies by model. GPT-4 may use \~3 chars/token, Claude \~4 chars/token. ### Cost Estimation Use token estimates to calculate costs: 1. Check manifest token estimate 2. Look up model pricing (input tokens) 3. Calculate: `tokens × (price / 1M tokens)` Example with GPT-4o: * 12,000 chars = \~3,000 tokens * GPT-4o input: \$2.50 / 1M tokens * Cost: 3,000 × ($2.50 / 1M) = $0.0075 (\~\$0.01) ## Folder Attachments Attaching a folder includes all files within it (recursively). ### Folder Behavior * **Recursive**: All subdirectories included * **File limit**: Up to 20,000 files per folder * **Sorting**: Files sorted alphabetically * **Excludes**: Hidden files and `node_modules` ### Example Attaching `projects/` folder: ``` projects/ glyph/ roadmap.md tasks.md notes/ 2024-01-15.md ``` Context includes: 1. Folder header: `# Folder: projects` 2. `projects/glyph/roadmap.md` 3. `projects/glyph/tasks.md` 4. `projects/notes/2024-01-15.md` Files are concatenated until character budget is exhausted. ## File Limits ### Per-File Limits When reading files for context: * **Max file size**: No hard limit (entire file read) * **Character budget**: Shared across all files * **UTF-8 only**: Binary files skipped ### Total Limits * **Character budget**: 200 - 250,000 chars * **File count**: Up to 20,000 files per folder * **Memory**: Limited by character budget, not file count ## Security and Privacy ### Path Safety * ✅ All paths restricted to current space * ❌ Path traversal blocked (`../` not allowed) * ❌ Hidden files excluded (files starting with `.`) * ❌ Symlinks outside space blocked From `src-tauri/src/ai_rig/context.rs:83`: ```rust theme={null} fn should_hide(name: &str) -> bool { name.starts_with('.') || name.eq_ignore_ascii_case("node_modules") } ``` ### No Secrets in Context Be careful when attaching folders that might contain: * `.env` files (excluded by default) * API keys in config files * Passwords or tokens in notes Review the manifest before sending to ensure no sensitive data is included. ## Use Cases ### Research and Summarization **Scenario**: Summarize all notes from a project. ``` Attach: projects/glyph/ Prompt: Summarize the key features and roadmap items. ``` The AI reads all files in `projects/glyph/` and provides a summary. ### Question Answering **Scenario**: Answer a specific question about your notes. ``` Mention: @meeting-notes-2024-01-15.md Prompt: What action items were discussed? ``` The AI reads the meeting notes and extracts action items. ### Content Generation **Scenario**: Generate content based on existing notes. ``` Attach: research/ai-features.md, research/user-feedback.md Prompt: Write a blog post about AI integration based on these notes. ``` The AI uses the attached files as source material. ### Comparison **Scenario**: Compare two documents. ``` Attach: design-v1.md, design-v2.md Prompt: What changed between these two design docs? ``` The AI reads both files and highlights differences. ## Best Practices ### Start Small * Begin with specific files * Attach folders only when necessary * Use @mentions for targeted context ### Review Manifest * Check token estimates before sending * Ensure no truncation of critical content * Adjust budget if items are truncated ### Be Specific * Attach only relevant files * Avoid attaching entire space unless needed * Use folders for related content groups ### Monitor Costs * Large contexts = higher costs * Free tier models may have stricter limits * Ollama (local) has no token costs ## Troubleshooting ### "Context index failed to load" **Cause**: Error reading file tree. **Solution**: Ensure space is open and accessible. ### Files not appearing in context menu **Possible causes**: * File is hidden (starts with `.`) * File is in `node_modules` * File index hasn't loaded yet **Solution**: Wait for index to load or refresh AI panel. ### @mention not working **Cause**: File not in index or typo in filename. **Solution**: 1. Use context menu to verify file path 2. Ensure exact filename match (case-sensitive) 3. Use autocomplete if available ### Context truncated unexpectedly **Cause**: Character budget too low. **Solution**: Increase character budget in context settings. ### "Total chars exceeds budget" **Cause**: Attached files exceed 250K character limit. **Solution**: 1. Remove some attachments 2. Attach specific files instead of large folders 3. Split into multiple conversations ## History and Audit Context is preserved in conversation history: ### Storage Location ``` .glyph/ai_history/.json ``` ### History Structure ```json theme={null} { "version": 1, "job_id": "uuid", "title": "Conversation title", "created_at_ms": 1704067200000, "profile": {...}, "messages": [...], "context_manifest": { "items": [ { "kind": "file", "label": "roadmap.md", "chars": 2500, "est_tokens": 625, "truncated": false } ], "total_chars": 2500, "est_tokens": 625 }, "tool_events": [...] } ``` ### Reviewing History In AI panel: 1. Click **History** 2. Select a past conversation 3. View messages, context manifest, and tool usage 4. Resume conversation or start new based on context ## Next Steps Learn about chat vs create modes Configure AI profiles and models Manage multiple AI configurations Provider-specific guides # AI Assistant Overview Source: https://docs.glyphformac.com/ai/overview Integrate AI models from multiple providers into your Glyph workspace Glyph includes a built-in AI assistant that brings powerful language models directly into your note-taking workflow. Connect to multiple AI providers, attach context from your notes, and interact with AI models through chat and creation modes. ## Key Features ### Multiple Provider Support Connect to leading AI providers: * **OpenAI** - GPT-4, GPT-4o, GPT-4 Turbo, and more * **Anthropic** - Claude 3.5 Sonnet, Claude 3 Opus, Haiku * **Google Gemini** - Gemini Pro, Gemini Flash * **OpenRouter** - Access 100+ models through a single API * **Ollama** - Run models locally on your machine * **OpenAI-compatible** - Any OpenAI-compatible endpoint * **Codex (ChatGPT OAuth)** - ChatGPT integration via OAuth authentication ### Profile Management Create multiple AI profiles with different configurations: * Switch between providers and models instantly * Configure model-specific parameters like reasoning effort * Store API keys securely per-space in `.glyph/app/ai_secrets.json` * Set default profiles for quick access ### Context Attachment Ground AI responses in your notes: * Attach individual files or entire folders to conversations * Mention files with `@filename` syntax in your prompts * Configure character budgets (200-250,000 chars) * View token estimates before sending * Context is stored per-conversation for repeatability ### Chat vs Create Modes **Chat Mode** - Conversational interaction without tools * Back-and-forth dialogue with the model * No file system access * Faster responses * Best for questions, brainstorming, and discussion **Create Mode** (default) - AI with workspace tools * Access to file reading, searching, and listing tools * AI can explore your notes to answer questions * Tool usage tracked in timeline view * Best for research, summarization, and knowledge work ### History and Audit Trail Every conversation is automatically saved: * History stored in `.glyph/ai_history/` as JSON * Review past conversations and their context * Tool usage events logged per conversation * Export conversation data for compliance or analysis ## Quick Start Go to Settings → AI and select a provider from the available profiles. Each profile supports a different AI service. Add your API key or authenticate via OAuth (for Codex). Keys are stored securely per space. Choose from available models for your provider. Glyph fetches the latest model list from each API. Click the AI icon in the sidebar or use the keyboard shortcut to open the AI panel. Type your message and press Enter. Attach context with `@` mentions or the context menu. ## Architecture * **Frontend**: React components in `src/components/ai/` * **Backend**: Rust `ai_rig` module in `src-tauri/src/ai_rig/` * **AI Framework**: [Rig](https://github.com/0xPlaygrounds/rig) for unified provider interface * **Storage**: SQLite index + JSON files in `.glyph/` * **Tools**: Space-scoped filesystem operations (read, search, list) ## Security * API keys stored encrypted in space-local `ai_secrets.json` * Tool operations restricted to current space (no path traversal) * SSRF protection for custom base URLs * Optional `allow_private_hosts` flag for local models * No hidden file access (dotfiles blocked) * Request/response audit trail available ## Next Steps Configure your first AI profile Provider-specific setup guides Learn about chat vs create modes Attach notes and folders to conversations # AI Profile Management Source: https://docs.glyphformac.com/ai/profiles Create and manage multiple AI profiles in Glyph AI profiles allow you to configure and switch between different AI providers, models, and settings. Create profiles for different use cases, models, or accounts. ## What is an AI Profile? An AI profile stores: * **Provider** - Which AI service (OpenAI, Anthropic, Gemini, etc.) * **Model** - Which model to use (gpt-4o, claude-3.5-sonnet, etc.) * **Base URL** - API endpoint (optional, for custom endpoints) * **Headers** - Custom HTTP headers (optional) * **Settings** - Provider-specific options (reasoning effort, etc.) * **API Key** - Stored separately per space in `.glyph/app/ai_secrets.json` ## Default Profiles On first launch, Glyph creates 7 default profiles: 1. **OpenAI** - Empty model, requires API key 2. **OpenAI-compatible** - Points to `http://localhost:11434/v1` 3. **OpenRouter** - Empty model, requires API key 4. **Anthropic** - Empty model, requires API key 5. **Gemini** - Empty model, requires API key 6. **Ollama** - Empty model, allows private hosts 7. **Codex (ChatGPT OAuth)** - Model `codex`, OAuth authentication ## Creating Profiles Glyph doesn't currently expose profile creation in the UI. You must edit `ai.json` manually to create new profiles. ### Profile Storage Location ``` ~/.config/glyph/ai.json (Linux) ~/Library/Application Support/glyph/ai.json (macOS) %APPDATA%/glyph/ai.json (Windows) ``` ### Manual Profile Creation Close Glyph to avoid conflicts when editing `ai.json`. Navigate to the config directory and open `ai.json` in a text editor. Add a new profile object to the `profiles` array: ```json theme={null} { "profiles": [ { "id": "unique-uuid-here", "name": "My Custom Profile", "provider": "openai", "model": "gpt-4o", "base_url": null, "headers": [], "allow_private_hosts": false, "reasoning_effort": null } ], "active_profile_id": "unique-uuid-here" } ``` Generate a UUID at [uuidgenerator.net](https://www.uuidgenerator.net/). Save `ai.json` and reopen Glyph. Your new profile appears in Settings → AI. ## Profile Structure ### Required Fields ```typescript theme={null} interface AiProfile { id: string; // Unique UUID name: string; // Display name provider: AiProviderKind; // See providers below model: string; // Model ID (e.g., "gpt-4o") } ``` ### Optional Fields ```typescript theme={null} interface AiProfile { base_url?: string | null; // Custom endpoint headers?: AiHeader[]; // Custom HTTP headers allow_private_hosts?: boolean; // Allow http:// and private IPs reasoning_effort?: string | null; // For Codex reasoning models } ``` ### Provider Values ```typescript theme={null} type AiProviderKind = | "openai" | "openai_compat" | "openrouter" | "anthropic" | "gemini" | "ollama" | "codex_chatgpt"; ``` ### Custom Headers ```typescript theme={null} interface AiHeader { key: string; // Header name value: string; // Header value } ``` Example: ```json theme={null} { "headers": [ { "key": "X-Custom-Header", "value": "my-value" }, { "key": "Authorization", "value": "Bearer token" } ] } ``` ## Switching Profiles ### Via Settings UI Go to **Settings → AI**. Click the profile dropdown and select a profile. Set API key, select model, and adjust settings. The AI panel uses the active profile for all conversations. ### Active Profile Storage The active profile ID is stored in `ai.json`: ```json theme={null} { "profiles": [...], "active_profile_id": "uuid-of-active-profile" } ``` Changing profiles in the UI updates this field. ## Use Cases ### Multiple Accounts **Scenario**: You have personal and work OpenAI accounts. **Solution**: Create two OpenAI profiles with different names and API keys. ```json theme={null} [ { "id": "uuid-1", "name": "OpenAI (Personal)", "provider": "openai", "model": "gpt-4o" }, { "id": "uuid-2", "name": "OpenAI (Work)", "provider": "openai", "model": "gpt-4o-mini" } ] ``` Set different API keys for each in `.glyph/app/ai_secrets.json`: ```json theme={null} { "uuid-1": "sk-personal-key", "uuid-2": "sk-work-key" } ``` ### Model Variants **Scenario**: You want quick access to different models. **Solution**: Create profiles for each model. ```json theme={null} [ { "id": "uuid-1", "name": "GPT-4o (Fast)", "provider": "openai", "model": "gpt-4o-mini" }, { "id": "uuid-2", "name": "GPT-4 (Quality)", "provider": "openai", "model": "gpt-4" } ] ``` Switch profiles to use different models without reconfiguring. ### Local and Cloud **Scenario**: Use Ollama for private notes, OpenAI for general tasks. **Solution**: Keep both profiles configured and switch as needed. ### Provider Comparison **Scenario**: Compare responses from different providers. **Solution**: Create profiles for OpenAI, Anthropic, and Gemini with similar models. Switch profiles and ask the same question to compare. ## API Key Management ### Per-Profile API Keys API keys are stored per profile ID in `.glyph/app/ai_secrets.json`: ```json theme={null} { "profile-uuid-1": "sk-openai-key", "profile-uuid-2": "sk-ant-anthropic-key", "profile-uuid-3": "google-api-key" } ``` ### Setting API Keys In Settings → AI: 1. Select a profile 2. Click **Set API Key** 3. Paste your API key 4. Click **Save** The key is stored under the profile's UUID. ### Clearing API Keys Click **Clear API Key** to remove the key for the active profile. ### Security * Keys stored **per space**, not globally * Each space has independent `ai_secrets.json` * Add `.glyph/` to `.gitignore` to avoid committing keys * Keys never logged or sent to Glyph servers ## Profile Examples ### Azure OpenAI Profile ```json theme={null} { "id": "azure-uuid", "name": "Azure OpenAI", "provider": "openai_compat", "model": "gpt-4", "base_url": "https://your-resource.openai.azure.com/openai/deployments/gpt-4", "headers": [ { "key": "api-key", "value": "your-azure-api-key" }, { "key": "api-version", "value": "2024-02-15-preview" } ], "allow_private_hosts": false } ``` ### Local LLaMA Profile ```json theme={null} { "id": "llama-uuid", "name": "LLaMA 3.1 (Local)", "provider": "ollama", "model": "llama3.1:70b", "base_url": "http://localhost:11434/v1", "headers": [], "allow_private_hosts": true } ``` ### OpenRouter with Custom Headers ```json theme={null} { "id": "or-uuid", "name": "OpenRouter", "provider": "openrouter", "model": "anthropic/claude-3.5-sonnet", "base_url": null, "headers": [ { "key": "HTTP-Referer", "value": "https://glyph.app" }, { "key": "X-Title", "value": "Glyph" } ], "allow_private_hosts": false } ``` ## Troubleshooting ### Profile doesn't appear in UI **Cause**: Invalid JSON in `ai.json`. **Solution**: Validate JSON syntax at [jsonlint.com](https://jsonlint.com/). ### "Unknown profile" error **Cause**: Profile ID mismatch or profile deleted. **Solution**: Check that `active_profile_id` matches a profile ID in the `profiles` array. ### API key not working after switching profiles **Cause**: Each profile has its own API key. **Solution**: Set the API key for each profile separately. ### Profile settings not saving **Cause**: File permissions or Glyph config directory not writable. **Solution**: Check file permissions on `ai.json`. ### Duplicate profile names **Cause**: Multiple profiles with the same name. **Solution**: Profile IDs must be unique, but names can be the same. Rename for clarity. ## Best Practices ### Naming Conventions Use descriptive names: * ✅ `OpenAI (Personal)` * ✅ `GPT-4o-mini (Fast)` * ✅ `Claude 3.5 Sonnet (Research)` * ❌ `Profile 1` * ❌ `Test` ### Profile Organization Group profiles by: * **Provider**: OpenAI, Anthropic, Gemini * **Use case**: Research, coding, writing * **Speed**: Fast (mini models), Quality (full models) * **Cost**: Free (Ollama), Paid (OpenAI) ### Backup Profiles Backup `ai.json` before making changes: ```bash theme={null} cp ~/.config/glyph/ai.json ~/.config/glyph/ai.json.bak ``` Restore if needed: ```bash theme={null} cp ~/.config/glyph/ai.json.bak ~/.config/glyph/ai.json ``` ## Future Enhancements Potential profile features (not yet implemented): * Profile creation via UI * Profile import/export * Profile templates * Per-space profile overrides * Profile-specific system prompts ## Next Steps Configure your first AI profile Provider-specific setup guides Learn about chat vs create modes Attach notes to conversations # Anthropic Configuration Source: https://docs.glyphformac.com/ai/providers/anthropic Set up Anthropic Claude models in Glyph Connect Glyph to Anthropic's API to use Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku, and other models. ## Prerequisites * Anthropic API account: [console.anthropic.com](https://console.anthropic.com) * API key with appropriate permissions * Sufficient API credits ## Setup 1. Log in to [Anthropic Console](https://console.anthropic.com) 2. Navigate to **API Keys** 3. Click **Create Key** 4. Copy the key (starts with `sk-ant-`) Store your API key securely. Anthropic only shows it once. Go to **Settings → AI** and select the **Anthropic** profile. 1. Click **Set API Key** in the authentication section 2. Paste your Anthropic API key 3. Click **Save** The key is stored in `.glyph/app/ai_secrets.json` in your space directory. Click the **Model** dropdown. Glyph fetches available models from Anthropic's API. Popular models: * `claude-3-5-sonnet-20241022` - Latest Claude 3.5 Sonnet (recommended) * `claude-3-opus-20240229` - Most capable, best for complex tasks * `claude-3-sonnet-20240229` - Balanced performance and speed * `claude-3-haiku-20240307` - Fastest, most affordable Open the AI panel and send a test message. You should receive a response from Claude. ## Configuration ### Provider Settings * **Service**: `anthropic` * **Base URL**: `https://api.anthropic.com` (default) * **Authentication**: `x-api-key` header * **API Version**: `2023-06-01` (automatically set) ### Custom Endpoint To use a custom Anthropic endpoint (proxy, self-hosted): 1. Set **Base URL** to your endpoint (e.g., `https://your-proxy.com`) 2. Add any required headers in **Custom Headers** 3. Enable **Allow Private Hosts** if using localhost ## Model Selection Glyph fetches the latest model list from Anthropic's `/v1/models` endpoint. ### Recommended Models | Model | Use Case | Context Window | Max Output | | ---------------------------- | --------------------------- | -------------- | ---------- | | `claude-3-5-sonnet-20241022` | General purpose, coding | 200K tokens | 8K tokens | | `claude-3-opus-20240229` | Complex reasoning, analysis | 200K tokens | 4K tokens | | `claude-3-sonnet-20240229` | Balanced tasks | 200K tokens | 4K tokens | | `claude-3-haiku-20240307` | Fast, simple tasks | 200K tokens | 4K tokens | Claude models have a **200K token context window**, allowing you to attach large amounts of context from your notes. ### Max Tokens Requirement Anthropic requires the `max_tokens` parameter for all requests. Glyph automatically sets this to **2048 tokens** for Anthropic models. If you need longer responses, this value is hardcoded in `src-tauri/src/ai_rig/runtime.rs:73`. ## Features ### Chat Mode Conversational interaction without tools: * Back-and-forth dialogue with Claude * No file system access * Faster responses * Best for discussion and brainstorming ### Create Mode Claude with workspace tools: * **read\_file** - Read files from your space * **search\_notes** - Search note content * **list\_dir** - List directory contents * Tool usage shown in timeline view * Best for research, summarization, and analysis ### Context Attachment Attach notes to leverage Claude's 200K context window: * Attach files or folders via context menu * Mention with `@filename` syntax * Configure character budget (up to 250K chars) * View token estimates before sending ## API Usage and Billing Glyph makes direct API calls to Anthropic: * You are billed by Anthropic based on token usage * No additional fees from Glyph * Check usage at [console.anthropic.com](https://console.anthropic.com) ### Cost Estimation | Model | Input (per 1M tokens) | Output (per 1M tokens) | | ----------------- | --------------------- | ---------------------- | | Claude 3.5 Sonnet | \$3.00 | \$15.00 | | Claude 3 Opus | \$15.00 | \$75.00 | | Claude 3 Sonnet | \$3.00 | \$15.00 | | Claude 3 Haiku | \$0.25 | \$1.25 | Use the context manifest to estimate costs before sending. ## Rate Limits Anthropic enforces rate limits based on your usage tier: * Limits vary by model and tier * Check your tier at [console.anthropic.com](https://console.anthropic.com) * Rate limit errors include retry-after headers If you hit rate limits, Glyph displays the error. Wait before retrying. ## Troubleshooting ### "API key not set for this profile" **Solution**: Add your Anthropic API key in Settings → AI. ### "model list failed (401)" **Solution**: Your API key is invalid or expired. Generate a new key from Anthropic Console. ### "model list failed (403)" **Solution**: Your API key doesn't have permission to list models. This is non-fatal; type the model ID manually. ### "model list failed (429)" **Solution**: You've hit Anthropic's rate limit. Wait before retrying. ### Model list is empty **Solution**: Type the model ID manually: * `claude-3-5-sonnet-20241022` * `claude-3-opus-20240229` * `claude-3-haiku-20240307` The model will work even if the list fetch failed. ### Responses are truncated **Cause**: Anthropic's `max_tokens` parameter (default 2048) limits output length. **Solution**: To increase output length, modify `src-tauri/src/ai_rig/runtime.rs:73` and rebuild: ```rust theme={null} let max_tokens = if caps.requires_max_tokens { Some(4096) // Increase from 2048 } else { None }; ``` ### "context length exceeded" **Cause**: Total tokens (context + conversation) exceeds 200K. **Solution**: Reduce attached context or use a smaller character budget. ## Security Best Practices * Never commit `.glyph/app/ai_secrets.json` to version control * Rotate API keys if exposed * Use separate keys for different projects * Monitor usage in Anthropic Console ## Claude-Specific Tips ### Extended Thinking Claude models excel at reasoning tasks. In create mode, Claude can: * Read multiple files to synthesize information * Search your notes for relevant context * Chain multiple tool calls to answer complex questions ### System Prompts Claude respects system prompts well. In create mode, Glyph adds: ``` Tool discipline for this run: use the minimum number of tool calls needed. Prefer at most 1-2 search/list calls before answering. If a tool returns usable evidence, stop searching and summarize. ``` This reduces unnecessary tool usage and improves response speed. ### Markdown and Code Claude generates well-formatted markdown and code. Responses render beautifully in Glyph's AI panel. ## Next Steps Learn about chat vs create modes Attach notes to leverage Claude's 200K context Compare with OpenAI GPT models Manage multiple AI profiles # Codex ChatGPT OAuth Source: https://docs.glyphformac.com/ai/providers/codex Connect to ChatGPT using Codex OAuth authentication Codex provides OAuth-based access to ChatGPT models, allowing you to use your existing ChatGPT Plus or Team subscription without separate API keys. ## Prerequisites * Codex CLI installed: [codexlang.com](https://codexlang.com) * ChatGPT Plus or Team subscription (optional, but provides access to better models) * Codex must be in your system PATH ## Setup Install Codex from [codexlang.com](https://codexlang.com): ```bash theme={null} curl -fsSL https://codexlang.com/install.sh | sh ``` Download the installer from [codexlang.com](https://codexlang.com) and add to PATH. Verify installation: ```bash theme={null} codex --version ``` Test that Codex app-server works: ```bash theme={null} codex app-server ``` You should see JSON-RPC output. Press Ctrl+C to exit. Go to **Settings → AI** and select the **Codex (ChatGPT OAuth)** profile. 1. Click **Connect** in the Codex account section 2. Your browser opens to ChatGPT OAuth consent page 3. Log in and authorize Codex 4. Return to Glyph 5. Click **Refresh** to verify connection You should see your email and account status as "connected". Click the **Model** dropdown. Codex fetches models available to your ChatGPT account. Available models depend on your subscription: * **Free tier**: `gpt-3.5-turbo`, `gpt-4o-mini` * **Plus/Team**: `gpt-4o`, `o1`, `o1-mini`, `o3-mini` For reasoning models (`o1`, `o3-mini`), select a reasoning effort: * **low** - Faster responses * **medium** - Balanced (default) * **high** - More thorough reasoning Open the AI panel and send a test message. You should receive a response from ChatGPT. ## How It Works Glyph runs Codex as a managed child process: 1. When Glyph starts, it launches `codex app-server` via stdio 2. Communication happens over JSON-RPC 3. OAuth tokens stored by Codex (not in Glyph) 4. Glyph sends chat requests through Codex 5. Codex streams responses back to Glyph ## Configuration ### Provider Settings * **Service**: `codex_chatgpt` * **Model**: Set automatically from Codex (e.g., `gpt-4o`, `o1-mini`) * **Authentication**: OAuth via Codex (no API key) * **Reasoning Effort**: Available for reasoning models ### Account Management #### Check Connection Status In Settings → AI → Codex section, you'll see: * **Status**: `connected` or `disconnected` * **Email**: Your ChatGPT account email * **Display Name**: Your ChatGPT account name * **Auth Mode**: `chatgpt` or similar #### Refresh Status Click **Refresh** to update account status and rate limits. #### Disconnect Click **Disconnect** to log out of ChatGPT. This clears OAuth tokens stored by Codex. ### Rate Limits Codex reports ChatGPT rate limits in Glyph settings: * **Requests per hour** * **Requests per day** * **Usage percentage** * **Reset time** Rate limits refresh automatically every 30 seconds. ## Reasoning Models ### Available Reasoning Models * `o1` - OpenAI's original reasoning model * `o1-mini` - Faster, lighter reasoning model * `o3-mini` - Latest reasoning model (if available) ### Reasoning Effort Levels Select reasoning effort in Settings → AI: | Level | Description | Use Case | | ---------- | -------------------------------- | --------------------------- | | **low** | Quick thinking, faster responses | Simple tasks, iterations | | **medium** | Balanced reasoning (default) | General problem-solving | | **high** | Deep thinking, slower responses | Complex math, logic puzzles | Reasoning models show thinking process in the AI panel before providing answers. ## Features ### Chat Mode Conversational interaction: * Back-and-forth dialogue * No file system access * Uses ChatGPT's web interface rate limits * Best for Q\&A and brainstorming ### Create Mode ChatGPT with workspace tools: * **read\_file** - Read files from your space * **search\_notes** - Search note content * **list\_dir** - List directory contents * Tool usage tracked in timeline view * Best for research and knowledge retrieval ### Context Attachment Attach notes for grounded responses: * Attach files or folders via context menu * Mention with `@filename` syntax * Configure character budget (up to 250K chars) * Context sent through Codex to ChatGPT ## Billing and Costs Codex uses your existing ChatGPT subscription: * ✅ No separate API charges * ✅ No token-by-token billing * ✅ Flat subscription rate (Plus: $20/month, Team: $25/user/month) * ✅ Unlimited messages within rate limits Codex/ChatGPT is ideal if you already have ChatGPT Plus or Team and want to avoid per-token API charges. ## Troubleshooting ### "failed to start codex app-server" **Cause**: Codex is not installed or not in PATH. **Solution**: 1. Verify installation: `codex --version` 2. Ensure `codex` is in your system PATH 3. Try running `codex app-server` manually to check for errors ### "codex request timed out" **Possible causes**: * Network connectivity issues * Codex app-server crashed * ChatGPT API is down **Solution**: 1. Restart Glyph (this restarts Codex app-server) 2. Check network connectivity 3. Try disconnecting and reconnecting your account ### Login opens browser but status shows disconnected **Solution**: 1. Click **Refresh** in AI Settings 2. Wait a few seconds for Codex to complete login 3. Check that you authorized in the browser 4. Some OAuth flows complete asynchronously ### "AI responses fail immediately" **Checklist**: 1. ✅ Profile provider is `Codex (ChatGPT OAuth)` 2. ✅ Model is selected (not empty) 3. ✅ Account status is `connected` 4. ✅ Rate limits not exceeded **Solution**: Disconnect and reconnect account, or restart Glyph. ### No streaming text appears **Cause**: Codex app-server may have exited or stopped sending notifications. **Solution**: Restart Glyph to restart Codex app-server. ### Rate limit exceeded **Cause**: You've hit ChatGPT's hourly or daily message limits. **Solution**: Wait for the rate limit to reset (shown in settings). ChatGPT Plus has higher limits than free tier. ### Model list is empty **Cause**: Codex couldn't fetch models from ChatGPT. **Solution**: 1. Verify account is connected 2. Click **Refresh** 3. Check that you're logged into ChatGPT in your browser 4. Restart Glyph ## Advanced Configuration ### Codex Logs Codex app-server logs are managed by Glyph. Check Glyph's logs for Codex communication details. ### Custom Codex Path If Codex is not in PATH, you'll need to add it. Glyph doesn't support custom Codex paths in the UI. **macOS/Linux**: ```bash theme={null} export PATH="$PATH:/path/to/codex" ``` **Windows**: Add Codex directory to System PATH in Environment Variables. ## Operational Notes * Glyph runs Codex app-server as a managed child process over stdio JSON-RPC * OAuth is started via `account/login/start` with `type: "chatgpt"` * Chat requests use `thread/start` + `turn/start` and stream via notifications * Authentication tokens stored by Codex, not Glyph * Codex process lifetime is tied to Glyph (restarts with app) ## Security * OAuth tokens stored by Codex CLI (not in `.glyph/app/ai_secrets.json`) * Codex handles authentication flow * No API keys stored in Glyph * Communication over local stdio (not network) ## Codex vs OpenAI API ### Use Codex ChatGPT OAuth When: ✅ You have ChatGPT Plus or Team subscription ✅ You want flat-rate billing (no per-token charges) ✅ You prefer OAuth over API keys ✅ You want access to reasoning models included in Plus ### Use OpenAI API When: ✅ You need programmatic access for automation ✅ You want higher rate limits ✅ You need fine-tuned models ✅ You prefer pay-as-you-go billing ## Next Steps Learn about chat vs create modes Attach notes to conversations Compare with direct OpenAI API integration Codex-specific troubleshooting # Google Gemini Configuration Source: https://docs.glyphformac.com/ai/providers/gemini Set up Google Gemini models in Glyph Connect Glyph to Google's Gemini API to use Gemini Pro, Gemini Flash, and other models. ## Prerequisites * Google AI Studio account: [aistudio.google.com](https://aistudio.google.com) * API key (free tier available) ## Setup 1. Visit [Google AI Studio](https://aistudio.google.com/app/apikey) 2. Click **Get API key** 3. Create a new API key or use an existing one 4. Copy the key Google AI Studio offers a free tier with generous limits for testing. Go to **Settings → AI** and select the **Gemini** profile. 1. Click **Set API Key** in the authentication section 2. Paste your Google API key 3. Click **Save** The key is stored in `.glyph/app/ai_secrets.json` in your space directory. Click the **Model** dropdown. Glyph fetches available models from Google's API. Popular models: * `gemini-1.5-pro` - Most capable Gemini model * `gemini-1.5-flash` - Fast and efficient * `gemini-pro` - Original Gemini Pro Open the AI panel and send a test message. You should receive a response from Gemini. ## Configuration ### Provider Settings * **Service**: `gemini` * **Base URL**: `https://generativelanguage.googleapis.com` (default) * **Authentication**: API key via query parameter ### API Endpoint Glyph uses the `/v1beta/models` endpoint to list models and sends requests to the Gemini API. The API key is passed as a query parameter: `?key=YOUR_API_KEY` ## Model Selection Glyph fetches the latest model list from Google's API. ### Recommended Models | Model | Use Case | Context Window | | ------------------ | ----------------------------- | -------------- | | `gemini-1.5-pro` | Complex reasoning, multimodal | 2M tokens | | `gemini-1.5-flash` | Fast tasks, high throughput | 1M tokens | | `gemini-pro` | General purpose | 32K tokens | **Gemini 1.5 Pro** has a **2 million token context window**, the largest of any production AI model. Attach entire codebases or books to your conversations. ### Model Naming Google's API returns model names with a `models/` prefix (e.g., `models/gemini-1.5-pro`). Glyph automatically strips this prefix when displaying and selecting models. ## Features ### Chat Mode Conversational interaction: * Back-and-forth dialogue with Gemini * No file system access * Fast responses * Best for Q\&A and brainstorming ### Create Mode Gemini with workspace tools: * **read\_file** - Read files from your space * **search\_notes** - Search note content * **list\_dir** - List directory contents * Tool usage tracked in timeline view * Best for research and knowledge retrieval ### Context Attachment Leverage Gemini's massive context window: * Attach files or entire folders * Mention with `@filename` syntax * Configure character budget (up to 250K chars) * Gemini can handle extremely large contexts ## API Usage and Billing ### Free Tier Google AI Studio offers a generous free tier: * **Gemini 1.5 Flash**: 15 RPM, 1M TPM, 1,500 RPD * **Gemini 1.5 Pro**: 2 RPM, 32K TPM, 50 RPD RPM = requests per minute, TPM = tokens per minute, RPD = requests per day ### Paid Tier (Pay-as-you-go) | Model | Input (per 1M tokens) | Output (per 1M tokens) | | ---------------------- | --------------------- | ---------------------- | | Gemini 1.5 Flash | \$0.075 | \$0.30 | | Gemini 1.5 Pro (≤128K) | \$1.25 | \$5.00 | | Gemini 1.5 Pro (>128K) | \$2.50 | \$10.00 | Check current pricing at [ai.google.dev/pricing](https://ai.google.dev/pricing). ## Rate Limits Rate limits depend on your tier and model: * Free tier: See limits above * Paid tier: Higher limits, see Google AI documentation If you hit rate limits, Glyph displays the error. Wait before retrying or upgrade to paid tier. ## Troubleshooting ### "API key not set for this profile" **Solution**: Add your Google API key in Settings → AI. ### "model list failed (400)" **Possible causes**: * Invalid API key * API key doesn't have permission for Gemini API **Solution**: Create a new API key from [AI Studio](https://aistudio.google.com/app/apikey). ### "model list failed (429)" **Solution**: You've hit Google's rate limit. Wait before retrying or check your quota. ### Model list is empty **Solution**: Type the model ID manually: * `gemini-1.5-pro` * `gemini-1.5-flash` * `gemini-pro` **Do not** include the `models/` prefix. ### "The model: `models/gemini-1.5-pro` does not exist" **Cause**: You included the `models/` prefix in the model field. **Solution**: Use just `gemini-1.5-pro` without the prefix. Glyph handles the prefix internally. ### Responses are slow with large context **Cause**: While Gemini supports huge contexts (2M tokens), processing them takes time. **Solution**: * Use `gemini-1.5-flash` for faster responses * Reduce context size if not all content is necessary * Be patient; processing 100K+ tokens may take 10-30 seconds ## Multimodal Support Gemini models support text, image, audio, and video inputs. However, Glyph currently only supports text inputs and outputs. Image and multimodal support may be added in a future release. ## Security Best Practices * Never commit `.glyph/app/ai_secrets.json` to version control * Rotate API keys if exposed * Monitor usage in [Google Cloud Console](https://console.cloud.google.com) * Set up billing alerts if using paid tier ## Gemini-Specific Tips ### Large Context Use Cases Gemini's 2M token context enables unique workflows: * Attach entire project directories * Include multiple books or research papers * Provide comprehensive context for analysis ### System Instructions Gemini respects system prompts. In create mode, Glyph adds tool usage guidelines to reduce unnecessary searches. ### Thinking Models Google may release reasoning models (similar to OpenAI's o1). When available, they'll appear in Glyph's model list automatically. ## Next Steps Learn about chat vs create modes Attach large contexts with Gemini's 2M token window Compare with Claude models Manage multiple AI profiles # Ollama Configuration Source: https://docs.glyphformac.com/ai/providers/ollama Run AI models locally with Ollama in Glyph Run open-source AI models locally on your machine using Ollama. No API keys, no cloud dependencies, complete privacy. ## Prerequisites * Ollama installed: [ollama.ai](https://ollama.ai) * Sufficient disk space for models (2-10GB per model) * Adequate RAM (8GB minimum, 16GB+ recommended) ## Setup Download and install Ollama from [ollama.ai](https://ollama.ai). ```bash theme={null} brew install ollama ollama serve ``` ```bash theme={null} curl -fsSL https://ollama.ai/install.sh | sh ollama serve ``` Download the installer from [ollama.ai/download](https://ollama.ai/download) and run it. Download a model from Ollama's library: ```bash theme={null} # Fast, capable model ollama pull llama3.2 # Larger, more capable ollama pull llama3.1:70b # Code-focused ollama pull codellama # Lightweight ollama pull phi3 ``` View all models at [ollama.ai/library](https://ollama.ai/library). Check that Ollama is running on `localhost:11434`: ```bash theme={null} curl http://localhost:11434/v1/models ``` You should see a JSON response with your installed models. Go to **Settings → AI** and select the **Ollama** profile. The default base URL is `http://localhost:11434/v1`. If Ollama runs on a different host or port, update the base URL. **Allow Private Hosts** is enabled by default for Ollama. Click the **Model** dropdown. Glyph fetches models from Ollama's local API. Select your downloaded model (e.g., `llama3.2`, `llama3.1:70b`). Open the AI panel and send a test message. You should receive a response from your local model. ## Configuration ### Provider Settings * **Service**: `ollama` * **Base URL**: `http://localhost:11434/v1` (default) * **Authentication**: None (local API) * **Allow Private Hosts**: Enabled (required for localhost) ### Custom Port If Ollama runs on a different port: ``` Base URL: http://localhost:8080/v1 ``` ### Remote Ollama Server To connect to Ollama on another machine: ``` Base URL: http://192.168.1.100:11434/v1 ``` Ensure **Allow Private Hosts** is enabled. ## Model Selection Glyph uses Ollama's OpenAI-compatible `/v1/models` endpoint to list models. ### Recommended Models | Model | Size | Use Case | RAM Required | | -------------- | ---- | -------------------- | ------------ | | `llama3.2` | 3B | Fast, everyday tasks | 8GB | | `llama3.1` | 8B | General purpose | 8GB | | `llama3.1:70b` | 70B | Most capable | 32GB+ | | `mistral` | 7B | Balanced performance | 8GB | | `codellama` | 7B | Code generation | 8GB | | `phi3` | 3.8B | Lightweight | 4GB | | `gemma2` | 9B | Google's open model | 8GB | Explore all models at [ollama.ai/library](https://ollama.ai/library). ### Model Tags Ollama models use tags for variants: * `llama3.1:latest` - Latest stable version * `llama3.1:70b` - 70 billion parameter variant * `llama3.1:8b-q4_0` - 4-bit quantized (smaller, faster) ## Features ### Chat Mode Conversational interaction: * Back-and-forth dialogue * No file system access * Fast local inference * Best for brainstorming and Q\&A ### Create Mode Local AI with workspace tools: * **read\_file** - Read files from your space * **search\_notes** - Search note content * **list\_dir** - List directory contents * Tool usage tracked in timeline view * Best for research and knowledge retrieval ### Context Attachment Attach notes for grounded responses: * Attach files or folders via context menu * Mention with `@filename` syntax * Configure character budget (up to 250K chars) * Context sent locally, never leaves your machine ## Performance ### Inference Speed Local inference speed depends on: * **Model size**: Smaller models (3B-8B) are faster * **Hardware**: GPU acceleration significantly improves speed * **Context length**: Longer contexts increase latency ### GPU Acceleration Ollama automatically uses GPU if available: * **NVIDIA**: CUDA support * **AMD**: ROCm support * **Apple Silicon**: Metal acceleration Check GPU usage: ```bash theme={null} ollama ps ``` ### Context Window Ollama models have varying context windows: * `llama3.1`: 128K tokens * `mistral`: 32K tokens * `codellama`: 16K tokens Larger contexts increase memory usage and latency. ## Privacy and Security Ollama runs entirely on your machine: * ✅ No data sent to external servers * ✅ No API keys required * ✅ Complete privacy for sensitive notes * ✅ Works offline * ✅ No usage limits or billing Ollama is ideal for private notes, confidential documents, or offline environments. ## Troubleshooting ### "model list failed" **Cause**: Glyph can't connect to Ollama. **Solution**: 1. Verify Ollama is running: `ollama ps` 2. Check the base URL in settings 3. Ensure **Allow Private Hosts** is enabled 4. Test connection: `curl http://localhost:11434/v1/models` ### Model not in dropdown **Solution**: Type the model name manually (e.g., `llama3.2`, `codellama`). ### "connection refused" **Cause**: Ollama is not running. **Solution**: Start Ollama: ```bash theme={null} ollama serve ``` ### Responses are very slow **Possible causes**: * Large model (70B+) without sufficient RAM * No GPU acceleration * Long context **Solutions**: * Use a smaller model (`llama3.2`, `phi3`) * Enable GPU acceleration (automatic if hardware supports it) * Reduce context size * Close other memory-intensive applications ### "out of memory" **Cause**: Model is too large for available RAM. **Solution**: * Use a smaller model * Use quantized variants (e.g., `llama3.1:8b-q4_0`) * Close other applications * Increase system swap space ### Tool calls fail in create mode **Cause**: Some Ollama models don't support function calling well. **Solution**: Use chat mode instead, or try a different model. `llama3.1` has good tool support. ## Advanced Configuration ### Custom Ollama Endpoint If you run Ollama with custom settings: ```bash theme={null} OLLAMA_HOST=0.0.0.0:8080 ollama serve ``` Update base URL in Glyph: ``` Base URL: http://localhost:8080/v1 ``` ### Model Parameters To adjust model parameters (temperature, top\_p, etc.), you'll need to modify Glyph's source code or use a different provider (OpenAI-compatible supports more options). ### Multiple Ollama Instances Run multiple Ollama instances on different ports and create separate profiles in Glyph for each. ## Next Steps Learn about chat vs create modes Attach notes to local AI conversations Use other OpenAI-compatible endpoints Manage multiple AI profiles # OpenAI Configuration Source: https://docs.glyphformac.com/ai/providers/openai Set up OpenAI GPT models in Glyph Connect Glyph to OpenAI's API to use GPT-4, GPT-4o, GPT-4 Turbo, and other models. ## Prerequisites * OpenAI API account: [platform.openai.com](https://platform.openai.com) * API key with appropriate permissions * Sufficient API credits ## Setup 1. Log in to [OpenAI Platform](https://platform.openai.com) 2. Navigate to **API Keys** in your account settings 3. Click **Create new secret key** 4. Copy the key (starts with `sk-`) Store your API key securely. OpenAI only shows it once. Go to **Settings → AI** and select the **OpenAI** profile. 1. Click **Set API Key** in the authentication section 2. Paste your OpenAI API key 3. Click **Save** The key is stored in `.glyph/app/ai_secrets.json` in your space directory. Click the **Model** dropdown. Glyph fetches available models from OpenAI's API. Popular models: * `gpt-4o` - Latest GPT-4 Omni (recommended) * `gpt-4o-mini` - Faster, more affordable GPT-4 * `gpt-4-turbo` - GPT-4 Turbo with vision * `gpt-4` - Original GPT-4 * `gpt-3.5-turbo` - Fast and cost-effective Open the AI panel and send a test message. You should receive a response from your selected model. ## Configuration ### Provider Settings * **Service**: `openai` * **Base URL**: `https://api.openai.com/v1` (default) * **Authentication**: Bearer token (API key) ### Custom Endpoint To use a custom OpenAI endpoint (proxy, Azure OpenAI, etc.): 1. Set **Base URL** to your endpoint 2. Add any required headers in **Custom Headers** 3. Enable **Allow Private Hosts** if using localhost ```json theme={null} Base URL: https://.openai.azure.com/openai/deployments/ Headers: [ { "key": "api-key", "value": "your-azure-api-key" }, { "key": "api-version", "value": "2024-02-15-preview" } ] ``` ```json theme={null} Base URL: https://your-proxy.com/v1 Headers: (if required by proxy) ``` ## Model Selection Glyph fetches the latest model list from OpenAI's `/v1/models` endpoint. ### Recommended Models | Model | Use Case | Context Window | | --------------- | --------------------------- | -------------- | | `gpt-4o` | General purpose, multimodal | 128K tokens | | `gpt-4o-mini` | Fast, affordable | 128K tokens | | `gpt-4-turbo` | Advanced reasoning | 128K tokens | | `gpt-4` | Original GPT-4 | 8K tokens | | `gpt-3.5-turbo` | Simple tasks, speed | 16K tokens | Glyph displays models returned by the API. If a model isn't listed, type its ID manually in the model field. ### Chat Completion Models Only Glyph uses the `/v1/chat/completions` endpoint. Ensure your selected model supports chat completions. Models like `text-davinci-003` or `gpt-3.5-turbo-instruct` are **not** chat models. If you select one, you'll see: ``` Model 'gpt-3.5-turbo-instruct' is not chat-completions compatible. Select a chat model (e.g., gpt-4o, gpt-4-turbo, gpt-4o-mini). ``` ## Features ### Chat Mode Conversational interaction without tools: * Back-and-forth dialogue * Faster responses (no tool overhead) * Best for brainstorming and discussion ### Create Mode AI with workspace access: * File reading via `read_file` tool * Search notes with `search_notes` tool * List files with `list_dir` tool * Best for research and knowledge retrieval ### Context Attachment Attach files or folders to ground responses: * Attach via context menu in AI panel * Mention files with `@filename` syntax * Context sent in system message * Token estimates shown before sending ## API Usage and Billing Glyph makes direct API calls to OpenAI: * You are billed by OpenAI based on token usage * No additional fees from Glyph * Check usage at [platform.openai.com/usage](https://platform.openai.com/usage) ### Cost Estimation Use the context manifest to estimate costs: 1. Attach context in AI panel 2. View token estimate in manifest 3. Calculate cost using [OpenAI pricing](https://openai.com/pricing) Example: * 10K input tokens + 1K output tokens with `gpt-4o` * Input: 10,000 × $0.0025 / 1K = $0.025 * Output: 1,000 × $0.010 / 1K = $0.010 * Total: \~\$0.035 per request ## Rate Limits OpenAI enforces rate limits based on your usage tier: * **Free tier**: 3 requests/min, 200 requests/day * **Tier 1+**: Higher limits based on usage history If you hit rate limits, Glyph displays the error from OpenAI. Wait before retrying or upgrade your tier. ## Troubleshooting ### "API key not set for this profile" **Solution**: Add your OpenAI API key in Settings → AI. ### "model list failed (401)" **Solution**: Your API key is invalid or expired. Generate a new key from OpenAI Platform. ### "model list failed (429)" **Solution**: You've hit OpenAI's rate limit. Wait before retrying. ### "This model is not chat-completions compatible" **Solution**: Select a chat model like `gpt-4o`, `gpt-4-turbo`, or `gpt-4o-mini`. ### Model list is empty **Solution**: Type the model ID manually (e.g., `gpt-4o`). The model will work even if the list fetch failed. ### Responses are slow **Possible causes**: * Large context (10K+ tokens) * Complex tool usage in create mode * OpenAI API latency **Solution**: Try a faster model like `gpt-4o-mini` or reduce context size. ## Security Best Practices * Never commit `.glyph/app/ai_secrets.json` to version control * Rotate API keys if exposed * Use separate keys for different projects * Set spending limits in OpenAI dashboard ## Next Steps Learn about chat vs create modes Attach notes to conversations Access 100+ models via OpenRouter Manage multiple AI profiles # OpenRouter Configuration Source: https://docs.glyphformac.com/ai/providers/openrouter Access 100+ AI models through OpenRouter in Glyph OpenRouter provides unified API access to 100+ AI models from multiple providers including OpenAI, Anthropic, Google, Meta, and more. ## Prerequisites * OpenRouter account: [openrouter.ai](https://openrouter.ai) * API key (free credits available) * Credits for paid models (or use free models) ## Setup 1. Sign up at [openrouter.ai](https://openrouter.ai) 2. Navigate to **Keys** in your account 3. Click **Create Key** 4. Copy the key (starts with `sk-or-`) OpenRouter provides \$1 in free credits to test models. Go to **Settings → AI** and select the **OpenRouter** profile. 1. Click **Set API Key** in the authentication section 2. Paste your OpenRouter API key 3. Click **Save** The key is stored in `.glyph/app/ai_secrets.json` in your space directory. Click the **Model** dropdown. Glyph fetches 100+ available models from OpenRouter. Popular models: * `anthropic/claude-3.5-sonnet` * `openai/gpt-4o` * `google/gemini-pro-1.5` * `meta-llama/llama-3.1-70b-instruct` * `mistralai/mistral-large` Free models are marked in the OpenRouter model list. Open the AI panel and send a test message. You should receive a response from your selected model. ## Configuration ### Provider Settings * **Service**: `openrouter` * **Base URL**: `https://openrouter.ai/api/v1` (default) * **Authentication**: Bearer token (API key) ### Model Routing OpenRouter automatically routes your request to the best available provider for your selected model. ## Model Selection Glyph fetches the latest model list from OpenRouter's `/v1/models` endpoint. ### Model Details OpenRouter provides rich model metadata: * **Context length** - Maximum tokens per request * **Pricing** - Input and output token costs * **Modalities** - Text, image, audio support * **Max output tokens** - Maximum response length * **Supported parameters** - Available model options Hover over the info icon in the model selector to view these details. ### Recommended Models #### Free Models | Model | Provider | Use Case | | --------------------------------------- | -------- | --------------- | | `meta-llama/llama-3.1-8b-instruct:free` | Meta | General purpose | | `mistralai/mistral-7b-instruct:free` | Mistral | Fast tasks | | `google/gemini-flash-1.5:free` | Google | Multimodal | Free models have rate limits but are great for testing and light usage. #### Paid Models (High Performance) | Model | Input Cost | Output Cost | | ----------------------------------- | ---------------- | ---------------- | | `anthropic/claude-3.5-sonnet` | \$3/1M tokens | \$15/1M tokens | | `openai/gpt-4o` | \$2.50/1M tokens | \$10/1M tokens | | `google/gemini-pro-1.5` | \$1.25/1M tokens | \$5/1M tokens | | `meta-llama/llama-3.1-70b-instruct` | \$0.88/1M tokens | \$0.88/1M tokens | Check current pricing at [openrouter.ai/models](https://openrouter.ai/models). ## Features ### Chat Mode Conversational interaction: * Back-and-forth dialogue with any model * No file system access * Fast responses * Best for discussion and brainstorming ### Create Mode AI with workspace tools: * **read\_file** - Read files from your space * **search\_notes** - Search note content * **list\_dir** - List directory contents * Tool usage tracked in timeline view * Best for research and knowledge retrieval Tool support varies by model. Most modern models (GPT-4, Claude, Gemini) support function calling. ### Context Attachment Attach notes for grounded responses: * Attach files or folders via context menu * Mention with `@filename` syntax * Configure character budget (up to 250K chars) * Context limits vary by model ## API Usage and Billing ### Credits OpenRouter uses a credit system: * 1 credit = \$1 USD * Purchase credits at [openrouter.ai/credits](https://openrouter.ai/credits) * Free credits on signup ### Cost Estimation Use the model selector to view pricing before sending: 1. Select a model 2. Hover over the info icon 3. View input/output pricing 4. Estimate cost: (input\_tokens × input\_price) + (output\_tokens × output\_price) ### Rate Limits Rate limits depend on your credit balance and model: * Higher credit balance = higher rate limits * Free models have stricter limits * Check limits at [openrouter.ai/docs/limits](https://openrouter.ai/docs/limits) ## Troubleshooting ### "API key not set for this profile" **Solution**: Add your OpenRouter API key in Settings → AI. ### "model list failed (401)" **Solution**: Your API key is invalid. Generate a new key from OpenRouter. ### "insufficient credits" **Solution**: Add credits at [openrouter.ai/credits](https://openrouter.ai/credits) or use a free model. ### "model not found" **Cause**: The model ID is incorrect or the model is no longer available. **Solution**: Refresh the model list or check [openrouter.ai/models](https://openrouter.ai/models) for valid model IDs. ### Model list is very long **Solution**: Use the search/filter in the model selector. Type to filter by provider or model name. ### Responses from different providers **Cause**: OpenRouter routes to the best available provider for your model. If primary provider is unavailable, it uses a fallback. **Solution**: This is expected behavior. Check OpenRouter's dashboard for routing details. ## Advanced Features ### Provider Preferences Some OpenRouter models allow provider selection (e.g., `anthropic/claude-3.5-sonnet:beta` vs `anthropic/claude-3.5-sonnet`). Use the full model ID from [openrouter.ai/models](https://openrouter.ai/models) to specify routing. ### Custom Headers OpenRouter supports custom headers for advanced use cases: ```json theme={null} [ { "key": "HTTP-Referer", "value": "https://yourdomain.com" }, { "key": "X-Title", "value": "Glyph Integration" } ] ``` These headers may provide better rate limits and routing. ## Security Best Practices * Never commit `.glyph/app/ai_secrets.json` to version control * Rotate API keys if exposed * Monitor usage at [openrouter.ai/activity](https://openrouter.ai/activity) * Set spending limits in your OpenRouter account ## OpenRouter vs Direct Providers ### Advantages of OpenRouter ✅ Single API key for 100+ models ✅ Automatic provider failover ✅ Unified billing ✅ Model comparison and discovery ✅ Free models available ### When to Use Direct Providers * Lower latency (no routing layer) * Provider-specific features (e.g., Anthropic's thinking models) * Direct billing relationship * Higher rate limits with provider-native keys ## Next Steps Learn about chat vs create modes Attach notes to conversations Compare with direct OpenAI integration Manage multiple AI profiles # AI Setup and Configuration Source: https://docs.glyphformac.com/ai/setup Configure AI profiles, models, and authentication in Glyph Set up AI assistance in Glyph by creating profiles, configuring providers, and managing authentication credentials. ## Initial Setup Navigate to **Settings → AI** or press the settings keyboard shortcut and select the AI tab. Glyph creates default profiles for all supported providers. Select the profile for your preferred provider (OpenAI, Anthropic, Gemini, etc.). Each profile has provider-specific settings: * **Service**: The AI provider (OpenAI, Anthropic, Gemini, OpenRouter, Ollama, OpenAI-compatible, Codex) * **Model**: Select from available models (fetched from the provider) * **Base URL**: Custom endpoint (optional, for OpenAI-compatible or self-hosted) * **Headers**: Additional HTTP headers (optional) * **Allow Private Hosts**: Enable for local models (Ollama, localhost endpoints) Most providers require an API key: * **OpenAI, Anthropic, Gemini, OpenRouter**: API key required * **Ollama, OpenAI-compatible**: Optional (depends on endpoint) * **Codex**: OAuth authentication (no API key needed) API keys are stored in `.glyph/app/ai_secrets.json` in your space directory. Click the model dropdown to fetch and select from available models. The list is retrieved from the provider API. ## Profile Configuration ### Service Selection Each profile is associated with a single provider. To use multiple providers, create or switch between profiles. Available services: * `openai` - OpenAI GPT models * `anthropic` - Anthropic Claude models * `gemini` - Google Gemini models * `openrouter` - OpenRouter multi-model API * `ollama` - Ollama local models * `openai_compat` - Any OpenAI-compatible endpoint * `codex_chatgpt` - Codex ChatGPT OAuth ### Model Selection Click **Model** to open the model selector: 1. **Automatic Model List**: Glyph fetches available models from the provider API 2. **Search Models**: Type to filter the model list 3. **Model Details**: Hover over the info icon to view: * Context length * Pricing (for OpenRouter) * Supported parameters * Input/output modalities 4. **Manual Entry**: If model fetch fails, type the model ID manually ### Advanced Options #### Base URL Override the default API endpoint: * **OpenAI**: `https://api.openai.com/v1` * **Anthropic**: `https://api.anthropic.com` * **Gemini**: `https://generativelanguage.googleapis.com` * **OpenRouter**: `https://openrouter.ai/api/v1` * **Ollama**: `http://localhost:11434/v1` * **OpenAI-compatible**: `http://localhost:11434/v1` (default) Custom base URLs are validated for SSRF attacks. Enable **Allow Private Hosts** to use `http://localhost` or private IP addresses. #### Custom Headers Add additional HTTP headers to requests: ```json theme={null} [ { "key": "X-Custom-Header", "value": "my-value" }, { "key": "Authorization", "value": "Bearer custom-token" } ] ``` Headers are applied to all requests for this profile. #### Reasoning Effort (Codex only) For models that support reasoning modes (e.g., `o1`, `o3-mini`): * **low** - Faster responses, less thorough * **medium** - Balanced (default) * **high** - More thorough, slower The option appears only when using Codex with a reasoning-capable model. ## API Key Management ### Setting an API Key 1. Select a profile that requires an API key 2. Click **Set API Key** in the authentication section 3. Paste your API key 4. Click **Save** The key is encrypted and stored in `.glyph/app/ai_secrets.json`. Create or edit `.glyph/app/ai_secrets.json` in your space: ```json theme={null} { "": "sk-your-api-key-here" } ``` Profile IDs are UUIDs visible in the settings UI or `ai.json`. ### Security * API keys are stored **per space**, not globally * Keys are stored in `.glyph/app/ai_secrets.json` (add to `.gitignore`) * File uses atomic writes to prevent corruption * Keys are never logged or sent to Glyph servers * Each space has independent API key storage ### Clearing an API Key In Settings → AI, click **Clear API Key** for the active profile. This removes the key from `ai_secrets.json`. ## Profile Storage Profile configurations (excluding API keys) are stored in: ``` ~/.config/glyph/ai.json (Linux) ~/Library/Application Support/glyph/ai.json (macOS) %APPDATA%/glyph/ai.json (Windows) ``` Example `ai.json`: ```json theme={null} { "profiles": [ { "id": "uuid-here", "name": "OpenAI", "provider": "openai", "model": "gpt-4o", "base_url": null, "headers": [], "allow_private_hosts": false, "reasoning_effort": null } ], "active_profile_id": "uuid-here" } ``` ## Default Profiles On first launch, Glyph creates these default profiles: * **OpenAI** - Empty model, requires API key * **OpenAI-compatible** - Points to `http://localhost:11434/v1` * **OpenRouter** - Empty model, requires API key * **Anthropic** - Empty model, requires API key * **Gemini** - Empty model, requires API key * **Ollama** - Empty model, allows private hosts * **Codex (ChatGPT OAuth)** - Model `codex`, OAuth authentication ## Troubleshooting ### "API key not set for this profile" Add your API key in Settings → AI → Authentication. ### "Model list failed" Check: 1. API key is valid and has correct permissions 2. Network connectivity 3. Base URL is correct (if customized) 4. Provider service is operational ### "http base\_url blocked" Enable **Allow Private Hosts** in advanced settings to use `http://` URLs. ### Model dropdown is empty Type the model ID manually in the model field. The app will use it even if the model list fetch failed. ## Next Steps Configure OpenAI provider Configure Anthropic Claude Run models locally Learn about chat vs create modes # April 20, 2026 Source: https://docs.glyphformac.com/changelog/april-20-2026 Vim mode, llama.cpp support, floating AI panel, and more ## New features * **Vim mode for the editor.** Power users can now enable optional Vim keybindings in [Advanced Settings](/workspace/settings). Navigate, edit, and stay in flow with familiar Vim-style controls — including insert/normal modes, common motions, and editing commands. Disabled by default. * **Native llama.cpp support.** Run local LLMs with zero configuration using the new [llama.cpp AI provider](/ai/overview). Point Glyph at your local llama.cpp server and start chatting — no cloud required. * **Print notes to PDF.** You can now print any markdown note directly from the command palette. * **Adjustable editor width.** Choose between Compact, Comfortable, and Wide editor width modes in [Advanced Settings](/workspace/settings) to match how you like to read and write. * **Recent spaces menu.** Quickly switch between your workspaces from the sidebar or the menu bar — no more digging through the file system to find a recent space. * **Floating AI panel.** The AI assistant now lives in a freely resizable floating window instead of a fixed sidebar, giving you more control over your workspace layout. * **Resizable database columns.** Drag column borders in [database tables](/features/databases) to resize them. Your preferred widths are saved automatically. * **Inline database row renaming.** Double-click a row title in any database to rename it directly — the underlying file is renamed for you. * **Orange accent color.** A new accent color option is available in Appearance settings. ## Improvements * Smoother space switching with polished UI transitions * Refreshed card layouts in All Notes and Kanban views with better markdown previews * Improved scrollbar visibility and hover behavior across the app * Better tag and pill readability in databases * Consolidated file appearance settings into a single menu * Tidied up command palette layout and keyboard shortcut labels ## Bug fixes * Fixed an issue where switching AI providers could leave the panel in a broken state * Fixed llama.cpp streaming retry when the server uses a non-standard base URL # Architecture Overview Source: https://docs.glyphformac.com/development/architecture High-level architecture of Glyph ## Overview Glyph is an offline-first desktop note-taking app built with a hybrid architecture: * **Frontend**: React 19 + TypeScript + Vite + Tailwind 4 * **Backend**: Tauri 2 + Rust * **Editor**: TipTap (Markdown) * **AI**: Rig-backed multi-provider chat * **UI**: shadcn/ui + Radix UI + Motion * **Storage**: SQLite index + filesystem ## Application Structure ### Frontend (`src/`) The frontend is a single-page React application with a context-based state management architecture. ```typescript Entry Point theme={null} // src/main.tsx → App.tsx ``` ```typescript Context Providers theme={null} // All state managed via React Context - SpaceContext // Space path & lifecycle - FileTreeContext // Files, tags, active file - ViewContext // Active view document - UIContext // Sidebar, search, preview state - EditorContext // TipTap editor instance ``` ### Backend (`src-tauri/src/`) The Rust backend handles all filesystem operations, indexing, and AI integration. ```rust Core Modules theme={null} lib.rs / main.rs → Tauri setup, command registration space/ → Space lifecycle (open/close/create) space_fs/ → Filesystem operations notes/ → Note CRUD, frontmatter parsing index/ → SQLite FTS + tag indexing ai_rig/ → Multi-provider AI runtime ai_codex/ → Codex OAuth integration links/ → Link preview fetching database/ → Database view queries ``` ```rust Safety Modules theme={null} paths.rs → join_under() prevents traversal io_atomic.rs → Crash-safe atomic writes net.rs → SSRF prevention glyph_paths.rs → .glyph/ directory helpers ``` ## IPC Layer Communication between frontend and backend uses **typed Tauri commands**. Implement command in `src-tauri/src/` module ```rust src-tauri/src/space/commands.rs theme={null} #[tauri::command] pub fn space_open(path: String, state: State) -> Result { // Implementation } ``` Add to Tauri builder's invoke handler ```rust src-tauri/src/lib.rs theme={null} .invoke_handler(tauri::generate_handler![ space_open, space_close, // ... ]) ``` Define in `TauriCommands` interface ```typescript src/lib/tauri.ts theme={null} interface TauriCommands { space_open: CommandDef<{ path: string }, SpaceInfo>; // ... } ``` Always use typed `invoke()` wrapper ```typescript theme={null} import { invoke } from '@/lib/tauri'; const spaceInfo = await invoke('space_open', { path: '/path/to/space' }); ``` ## Data Flow ### File Operations ```mermaid theme={null} sequenceDiagram participant UI as React Component participant CTX as Context participant IPC as Tauri IPC participant FS as Rust Backend participant DB as SQLite Index UI->>CTX: User opens file CTX->>IPC: invoke('space_read_text') IPC->>FS: Read from filesystem FS->>IPC: File content + etag IPC->>CTX: Update active file CTX->>UI: Render editor FS->>DB: Update index (async) ``` ### Search Flow ```mermaid theme={null} sequenceDiagram participant UI as Search Input participant IPC as Tauri IPC participant IDX as SQLite FTS participant RANK as Hybrid Ranker UI->>IPC: invoke('search', { query }) IPC->>IDX: Full-text search IDX->>RANK: Raw results RANK->>IPC: Scored + ranked IPC->>UI: Display results ``` ## State Management ### React Context Architecture Glyph uses **no global state library** (no Redux/Zustand). All state is managed via React Context with the following hierarchy: ```typescript SpaceContext theme={null} // Root-level: Space lifecycle interface SpaceContextValue { spacePath: string | null; spaceSchemaVersion: number | null; onOpenSpace: () => Promise; onCreateSpace: () => Promise; closeSpace: () => Promise; } ``` ```typescript FileTreeContext theme={null} // File browser state interface FileTreeContextValue { rootEntries: FsEntry[]; childrenByDir: Record; expandedDirs: Set; activeFilePath: string | null; tags: TagCount[]; } ``` ```typescript EditorContext theme={null} // TipTap editor instance interface EditorContextValue { editor: Editor | null; isEditing: boolean; saveState: 'saved' | 'saving' | 'unsaved'; } ``` ## File System Layout Each space is a directory containing: ``` my-space/ ├── notes/ # Markdown files with YAML frontmatter │ └── example.md ├── assets/ # Content-addressed files (SHA256) │ └── abc123...def.png ├── cache/ # Link previews, thumbnails │ ├── links/ │ └── images/ ├── .glyph/ # App metadata (not in space root) │ ├── index.db # SQLite FTS + tags │ ├── ai_history.db # Chat history │ └── profiles.json # AI provider configs └── space.json # Schema version ``` The `.glyph/` folder stores derived data and can be safely deleted. It will be regenerated on next space open. ## Build & Bundle ### Development * `pnpm dev` - Vite dev server (frontend only) * `pnpm tauri dev` - Full Tauri app with hot reload ### Production * `pnpm build` - TypeScript check + Vite build * `pnpm tauri build` - Create native app bundle * **macOS**: `.dmg` + `.app` * **Windows**: `.msi` + `.exe` * **Linux**: `.deb` + `.AppImage` ## Security Architecture ### Path Traversal Prevention All space-relative paths are validated using `paths::join_under()`: ```rust src-tauri/src/paths.rs theme={null} pub fn join_under(base: &Path, rel: &str) -> Result { // Rejects ".." components to prevent traversal attacks } ``` ### SSRF Prevention User-supplied URLs (link previews) are checked before fetching: ```rust src-tauri/src/net.rs theme={null} pub fn check_user_url(url: &str, allow_private: bool) -> Result<(), String> { // Blocks private IPs unless explicitly allowed } ``` ### Atomic Writes All file writes use crash-safe atomic operations: ```rust src-tauri/src/io_atomic.rs theme={null} pub fn write_atomic(path: &Path, contents: &[u8]) -> io::Result<()> { // Write to temp → sync → rename → sync parent dir } ``` ## Migration Policy Glyph uses a **hard cutover migration approach**. When the space schema changes, old versions cannot open new spaces. Never implement backward compatibility. Version is stored in `space.json`: ```json theme={null} { "version": 1 } ``` # SQLite Indexing Source: https://docs.glyphformac.com/development/backend/indexing Full-text search and metadata indexing ## Overview Glyph uses **SQLite with FTS5** (Full-Text Search) to index all notes for fast searching. The index is stored in `.glyph/index.db` and includes: * Note content (full-text searchable) * Frontmatter properties (tags, custom fields) * Internal links (wikilinks + markdown links) * Task items (with due dates, scheduled dates) The index is **derived data**. It can be rebuilt from source files at any time via `index_rebuild` command. ## Schema ### Notes Table (FTS5) ```sql src-tauri/src/index/schema.rs theme={null} CREATE VIRTUAL TABLE notes_fts USING fts5( id UNINDEXED, -- Note path (e.g., 'notes/example.md') title, -- Note title (from frontmatter or filename) content, -- Full markdown content tokenize='porter' -- Porter stemming ("running" matches "run") ); ``` ### Tags Table ```sql theme={null} CREATE TABLE tags ( note_id TEXT NOT NULL, -- Foreign key to note path tag TEXT NOT NULL, -- Tag name (e.g., 'research') PRIMARY KEY (note_id, tag) ); CREATE INDEX idx_tags_tag ON tags(tag); ``` ### Links Table ```sql theme={null} CREATE TABLE links ( source_id TEXT NOT NULL, -- Source note path target_id TEXT NOT NULL, -- Target note path (resolved) link_type TEXT NOT NULL, -- 'wikilink' or 'markdown' PRIMARY KEY (source_id, target_id) ); CREATE INDEX idx_links_target ON links(target_id); ``` ### Tasks Table ```sql theme={null} CREATE TABLE tasks ( task_id TEXT PRIMARY KEY, -- Unique task ID note_id TEXT NOT NULL, -- Parent note path line_start INTEGER NOT NULL, -- Line number in note raw_text TEXT NOT NULL, -- Full task markdown checked BOOLEAN NOT NULL, -- Completion status status TEXT, -- Custom status (e.g., '> in progress') priority INTEGER, -- Priority level (1-3) due_date TEXT, -- ISO date (YYYY-MM-DD) scheduled_date TEXT, -- ISO date (YYYY-MM-DD) section TEXT -- Parent heading ); CREATE INDEX idx_tasks_note ON tasks(note_id); CREATE INDEX idx_tasks_due ON tasks(due_date); CREATE INDEX idx_tasks_scheduled ON tasks(scheduled_date); ``` ## Indexing Process ### Initial Index Build When a space is opened: ```rust src-tauri/src/index/db.rs theme={null} pub fn open_db(glyph_dir: &Path) -> Result { let db_path = glyph_dir.join("index.db"); let db = Connection::open(db_path)?; schema::ensure_schema(&db)?; // Create tables if missing Ok(db) } ``` ```rust src-tauri/src/index/indexer.rs theme={null} pub fn rebuild_index(space_root: &Path, db: &Connection) -> Result { let notes_dir = space_root.join("notes"); let mut indexed = 0; for entry in WalkDir::new(notes_dir) { let path = entry.path(); if path.extension() == Some(OsStr::new("md")) { index_note(path, db)?; indexed += 1; } } Ok(indexed) } ``` ```rust theme={null} fn index_note(path: &Path, db: &Connection) -> Result<(), String> { let content = fs::read_to_string(path)?; let (frontmatter, body) = parse_frontmatter(&content)?; // Extract metadata let title = frontmatter.get("title") .or_else(|| extract_first_heading(&body)) .unwrap_or_else(|| path.file_stem().to_string()); let tags = frontmatter.get("tags") .map(|v| parse_tag_list(v)) .unwrap_or_default(); // Index content index_note_content(db, path, &title, &body)?; index_tags(db, path, &tags)?; index_links(db, path, &body)?; index_tasks(db, path, &body)?; Ok(()) } ``` ### Incremental Updates The filesystem watcher triggers re-indexing on file changes: ```rust src-tauri/src/space/watcher.rs theme={null} let watcher = notify::recommended_watcher(move |event| { match event { Ok(Event { kind: EventKind::Modify(_), paths, .. }) => { for path in paths { if path.extension() == Some(OsStr::new("md")) { // Re-index single file indexer::reindex_file(&path, &db)?; } } } Ok(Event { kind: EventKind::Remove(_), paths, .. }) => { for path in paths { // Remove from index indexer::delete_from_index(&path, &db)?; } } _ => {} } })?; ``` ## Search Implementation ### Basic Search ```rust src-tauri/src/index/search_hybrid.rs theme={null} #[tauri::command] pub fn search( query: String, state: State, ) -> Result, String> { let current = state.current.lock().unwrap(); let space = current.as_ref().ok_or("No space open")?; let mut stmt = space.db.prepare(" SELECT id, title, snippet(notes_fts, 2, '', '', '...', 32) as snippet FROM notes_fts WHERE notes_fts MATCH ? ORDER BY rank LIMIT 50 ")?; let results = stmt.query_map([query], |row| { Ok(SearchResult { id: row.get(0)?, title: row.get(1)?, snippet: row.get(2)?, score: 1.0, // FTS5 rank is negative, normalize later }) })?.collect()?; Ok(results) } ``` ### Advanced Search ```rust src-tauri/src/index/search_advanced.rs theme={null} pub fn search_advanced( request: SearchAdvancedRequest, db: &Connection, ) -> Result, String> { let mut where_clauses = vec![]; let mut params: Vec> = vec![]; // Text query if let Some(query) = request.query { if request.title_only { where_clauses.push("title MATCH ?"); } else { where_clauses.push("notes_fts MATCH ?"); } params.push(Box::new(query)); } // Tag filter if let Some(tags) = request.tags { where_clauses.push(" id IN ( SELECT note_id FROM tags WHERE tag IN (" + placeholders(&tags) + ") GROUP BY note_id HAVING COUNT(DISTINCT tag) = ? ) "); for tag in &tags { params.push(Box::new(tag.clone())); } params.push(Box::new(tags.len())); } let sql = format!( "SELECT id, title, snippet(notes_fts, 2, '', '', '...', 32) FROM notes_fts WHERE {} ORDER BY rank LIMIT ?", where_clauses.join(" AND ") ); params.push(Box::new(request.limit.unwrap_or(50))); // Execute query... } ``` ## Tag Indexing ### Extracting Tags Tags come from two sources: 1. **Frontmatter**: `tags: [research, ai]` 2. **Inline hashtags**: `#research #ai` ```rust src-tauri/src/index/tags.rs theme={null} pub fn extract_tags(frontmatter: &HashMap, body: &str) -> Vec { let mut tags = HashSet::new(); // Frontmatter tags if let Some(Value::Array(arr)) = frontmatter.get("tags") { for v in arr { if let Value::String(s) = v { tags.insert(s.clone()); } } } // Inline hashtags let hashtag_re = Regex::new(r"#([\w-]+)").unwrap(); for cap in hashtag_re.captures_iter(body) { tags.insert(cap[1].to_string()); } tags.into_iter().collect() } pub fn index_tags( db: &Connection, note_id: &str, tags: &[String], ) -> Result<(), rusqlite::Error> { // Clear existing tags db.execute("DELETE FROM tags WHERE note_id = ?", [note_id])?; // Insert new tags let mut stmt = db.prepare("INSERT INTO tags (note_id, tag) VALUES (?, ?)")?; for tag in tags { stmt.execute([note_id, tag])?; } Ok(()) } ``` ### Tag Queries ```rust src-tauri/src/index/tags.rs theme={null} #[tauri::command] pub fn tags_list( limit: Option, state: State, ) -> Result, String> { let current = state.current.lock().unwrap(); let space = current.as_ref().ok_or("No space open")?; let mut stmt = space.db.prepare(" SELECT tag, COUNT(*) as count FROM tags GROUP BY tag ORDER BY count DESC, tag ASC LIMIT ? ")?; let tags = stmt.query_map([limit.unwrap_or(100)], |row| { Ok(TagCount { tag: row.get(0)?, count: row.get(1)?, }) })?.collect()?; Ok(tags) } ``` ## Link Indexing ### Extracting Links ```rust src-tauri/src/index/links.rs theme={null} pub fn extract_links(note_id: &str, body: &str) -> Vec { let mut links = vec![]; // Wikilinks: [[target]] or [[target|alias]] let wikilink_re = Regex::new(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]").unwrap(); for cap in wikilink_re.captures_iter(body) { links.push(Link { source: note_id.to_string(), target: cap[1].to_string(), link_type: "wikilink".to_string(), }); } // Markdown links: [text](href) let md_link_re = Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap(); for cap in md_link_re.captures_iter(body) { let href = &cap[2]; if !href.starts_with("http://") && !href.starts_with("https://") { links.push(Link { source: note_id.to_string(), target: href.to_string(), link_type: "markdown".to_string(), }); } } links } ``` ### Backlink Queries ```rust theme={null} #[tauri::command] pub fn backlinks( note_id: String, state: State, ) -> Result, String> { let current = state.current.lock().unwrap(); let space = current.as_ref().ok_or("No space open")?; let mut stmt = space.db.prepare(" SELECT DISTINCT n.id, n.title, n.updated FROM links l JOIN notes_fts n ON l.source_id = n.id WHERE l.target_id = ? ORDER BY n.updated DESC ")?; let backlinks = stmt.query_map([note_id], |row| { Ok(BacklinkItem { id: row.get(0)?, title: row.get(1)?, updated: row.get(2)?, }) })?.collect()?; Ok(backlinks) } ``` ## Task Indexing ### Parsing Tasks ```rust src-tauri/src/index/tasks/parse.rs theme={null} pub fn parse_tasks(note_id: &str, markdown: &str) -> Vec { let mut tasks = vec![]; let mut current_section = None; for (line_num, line) in markdown.lines().enumerate() { // Track headings for section context if line.starts_with('#') { current_section = Some(line.trim_start_matches('#').trim().to_string()); continue; } // Parse task: - [ ] or - [x] if let Some(task) = parse_task_line(line) { let (due_date, scheduled_date) = extract_dates(line); tasks.push(TaskItem { task_id: format!("{}-{}", note_id, line_num), note_id: note_id.to_string(), line_start: line_num, raw_text: line.to_string(), checked: task.checked, status: task.status, priority: task.priority, due_date, scheduled_date, section: current_section.clone(), }); } } tasks } ``` ### Task Queries ```rust src-tauri/src/index/tasks/store.rs theme={null} pub fn query_tasks( bucket: &str, today: &str, folders: Option<&[String]>, db: &Connection, ) -> Result, String> { let where_clause = match bucket { "inbox" => "scheduled_date IS NULL AND due_date IS NULL AND checked = 0", "today" => "(scheduled_date <= ? OR due_date = ?) AND checked = 0", "upcoming" => "scheduled_date > ? AND checked = 0", _ => return Err("Invalid bucket".to_string()), }; let sql = if let Some(folders) = folders { format!( "SELECT * FROM tasks WHERE {} AND note_id LIKE ?", where_clause ) } else { format!("SELECT * FROM tasks WHERE {}", where_clause) }; // Execute query... } ``` ## Performance Optimization ### FTS5 Optimization ```sql theme={null} -- Use 'optimize' to merge segments INSERT INTO notes_fts(notes_fts) VALUES('optimize'); ``` ```rust theme={null} pub fn optimize_index(db: &Connection) -> Result<(), rusqlite::Error> { db.execute("INSERT INTO notes_fts(notes_fts) VALUES('optimize')", [])?; Ok(()) } ``` ### Batch Inserts ```rust theme={null} pub fn index_notes_batch( notes: &[&Path], db: &Connection, ) -> Result<(), String> { let tx = db.transaction()?; for note in notes { index_note(note, &tx)?; } tx.commit()?; Ok(()) } ``` ## Next Steps Content-addressed file storage Learn IPC communication # File Storage System Source: https://docs.glyphformac.com/development/backend/storage Content-addressed storage and atomic writes ## Overview Glyph's storage system consists of: 1. **Space Files** - User-created markdown files in `notes/` 2. **Assets** - Content-addressed files in `assets/` (SHA256 hash) 3. **Cache** - Derived data in `cache/` (link previews, thumbnails) 4. **Atomic Writes** - Crash-safe file operations ## Content-Addressed Storage ### Why Content-Addressing? Storing files by their **SHA256 hash** provides: * **Deduplication** - Same file stored once, even if used in 100 notes * **Integrity** - Hash mismatch = corrupted file * **Immutability** - Content can't change without changing hash * **Cache-friendly** - Hash is permanent, perfect for CDNs ### Implementation ```rust src-tauri/src/notes/attachments.rs theme={null} use sha2::{Sha256, Digest}; use std::io; pub fn attach_file( note_id: &str, source_path: &Path, space_root: &Path, ) -> Result { // Read source file let mut file = File::open(source_path) .map_err(|e| format!("Failed to open file: {}", e))?; // Compute SHA256 hash while reading let mut hasher = Sha256::new(); let mut buffer = Vec::new(); io::copy(&mut file, &mut hasher)?; let hash = hex::encode(hasher.finalize()); // Get file extension let extension = source_path .extension() .and_then(|s| s.to_str()) .unwrap_or("bin"); // Asset filename: {hash}.{ext} let asset_name = format!("{}.{}", hash, extension); let asset_path = space_root.join("assets").join(&asset_name); // Only copy if not already exists (deduplication!) if !asset_path.exists() { fs::copy(source_path, &asset_path) .map_err(|e| format!("Failed to copy: {}", e))?; } // Generate markdown link (relative from note) let rel_path = relative_path(note_id, &format!("assets/{}", asset_name)); let markdown = format!("![]({})", rel_path); Ok(AttachmentResult { asset_rel_path: format!("assets/{}", asset_name), markdown, }) } ``` ### Example ```typescript Frontend theme={null} // User attaches logo.png to notes/project/readme.md const result = await invoke('note_attach_file', { note_id: 'notes/project/readme.md', source_path: '/Users/me/Downloads/logo.png' }); // Returns: // { // asset_rel_path: 'assets/a1b2c3d4e5f6...789.png', // markdown: '![](../../assets/a1b2c3d4e5f6...789.png)' // } // Markdown is inserted into editor at cursor editor.commands.insertContent(result.markdown); ``` If the same `logo.png` is attached to 10 different notes, only **one copy** exists on disk. ## Atomic Writes ### The Problem Naive file writes can corrupt data if: * App crashes mid-write * Disk full during write * Power loss during write ### The Solution Write to temp file → fsync → rename → fsync parent directory. ```rust src-tauri/src/io_atomic.rs theme={null} use std::fs; use std::io::{self, Write}; use std::path::Path; pub fn write_atomic(path: &Path, contents: &[u8]) -> io::Result<()> { // 1. Write to temporary file in same directory let temp_path = path.with_extension("tmp"); let mut file = fs::File::create(&temp_path)?; file.write_all(contents)?; // 2. Flush OS buffers to disk (critical!) file.sync_all()?; drop(file); // 3. Atomically rename temp to final path // This is atomic on all major filesystems (ext4, APFS, NTFS) fs::rename(&temp_path, path)?; // 4. Sync parent directory to persist rename if let Some(parent) = path.parent() { let parent_file = fs::File::open(parent)?; parent_file.sync_all()?; } Ok(()) } ``` ### Usage ```rust src-tauri/src/space_fs/read_write/text.rs theme={null} use crate::io_atomic::write_atomic; #[tauri::command] pub fn space_write_text( path: String, text: String, state: State, ) -> Result { let current = state.current.lock().unwrap(); let space = current.as_ref().ok_or("No space open")?; let abs_path = paths::join_under(&space.root, &path)?; // Atomic write - crash-safe! write_atomic(&abs_path, text.as_bytes()) .map_err(|e| format!("Write failed: {}", e))?; let metadata = fs::metadata(&abs_path)?; let mtime_ms = metadata.modified()? .duration_since(UNIX_EPOCH) .unwrap() .as_millis() as u64; Ok(TextFileWriteResult { etag: format!("{}-{}", mtime_ms, metadata.len()), mtime_ms, }) } ``` ## Path Safety ### Preventing Path Traversal All user-provided paths are validated: ```rust src-tauri/src/paths.rs theme={null} use std::path::{Path, PathBuf, Component}; pub fn join_under(base: &Path, rel: &str) -> Result { // Normalize path by stripping ".." components let normalized = PathBuf::from(rel) .components() .filter(|c| !matches!(c, Component::ParentDir)) .collect::(); let joined = base.join(&normalized); // Ensure result is still under base if !joined.starts_with(base) { return Err(format!( "Path traversal detected: '{}' escapes base '{}'", rel, base.display() )); } Ok(joined) } ``` ### Examples ```rust theme={null} let base = PathBuf::from("/space"); // ✅ Safe paths assert_eq!( join_under(&base, "notes/example.md"), Ok(PathBuf::from("/space/notes/example.md")) ); assert_eq!( join_under(&base, "./notes/../assets/file.png"), Ok(PathBuf::from("/space/assets/file.png")) ); // ❌ Rejected paths assert!(join_under(&base, "../../../etc/passwd").is_err()); assert!(join_under(&base, "/absolute/path").is_err()); ``` ## Cache System ### Link Preview Cache External link metadata is cached to avoid repeated fetching: ```rust src-tauri/src/links/cache.rs theme={null} use std::time::SystemTime; pub struct LinkCache { pub url: String, pub title: String, pub description: String, pub image_url: Option, pub fetched_at: SystemTime, } pub fn cache_link_preview( space_root: &Path, url: &str, preview: &LinkPreview, ) -> Result<(), io::Error> { let cache_dir = space_root.join("cache").join("links"); fs::create_dir_all(&cache_dir)?; // Hash URL to get cache filename let url_hash = sha256_str(url); let cache_file = cache_dir.join(format!("{}.json", url_hash)); let json = serde_json::to_vec_pretty(preview)?; write_atomic(&cache_file, &json)?; Ok(()) } pub fn read_cached_link( space_root: &Path, url: &str, ) -> Option { let cache_dir = space_root.join("cache").join("links"); let url_hash = sha256_str(url); let cache_file = cache_dir.join(format!("{}.json", url_hash)); if !cache_file.exists() { return None; } // Check if cache is stale (> 7 days) let metadata = fs::metadata(&cache_file).ok()?; let age = SystemTime::now() .duration_since(metadata.modified().ok()?) .ok()?; if age.as_secs() > 7 * 24 * 60 * 60 { return None; // Stale, re-fetch } let json = fs::read_to_string(&cache_file).ok()?; serde_json::from_str(&json).ok() } ``` ## Space Structure Summary ``` my-space/ ├── notes/ # User files (version control this) │ ├── daily/ │ │ └── 2024-03-15.md │ ├── projects/ │ │ └── project-x.md │ └── readme.md │ ├── assets/ # Content-addressed (version control this) │ ├── a1b2c3d4...789.png # SHA256 hash │ └── f4e5d6c7...012.pdf │ ├── cache/ # Derived data (DO NOT version control) │ ├── links/ # Link preview JSON files │ │ └── abc123.json │ └── images/ # Cached external images │ └── xyz789.jpg │ ├── .glyph/ # App data (DO NOT version control) │ ├── index.db # SQLite FTS index │ ├── ai_history.db # Chat history │ └── profiles.json # AI provider configs │ └── space.json # Schema version (version control this) ``` ### .gitignore Recommendation ```gitignore .gitignore theme={null} # Glyph derived data .glyph/ cache/ # Keep notes and assets !notes/ !assets/ !space.json ``` ## File Watching ### Filesystem Watcher ```rust src-tauri/src/space/watcher.rs theme={null} use notify::{RecommendedWatcher, RecursiveMode, Watcher, Event, EventKind}; use std::sync::mpsc::channel; pub fn start_watcher( space_root: &Path, db: Arc>, ) -> Result { let (tx, rx) = channel(); let mut watcher = RecommendedWatcher::new(tx, Config::default())?; // Watch notes directory recursively watcher.watch( &space_root.join("notes"), RecursiveMode::Recursive )?; // Spawn event handler thread std::thread::spawn(move || { for event in rx { match event { Ok(Event { kind: EventKind::Modify(_), paths, .. }) => { for path in paths { if path.extension() == Some(OsStr::new("md")) { // Re-index modified note let db = db.lock().unwrap(); let _ = indexer::reindex_file(&path, &db); } } } Ok(Event { kind: EventKind::Remove(_), paths, .. }) => { for path in paths { // Remove from index let db = db.lock().unwrap(); let _ = indexer::delete_from_index(&path, &db); } } _ => {} } } }); Ok(watcher) } ``` ## Performance Considerations ### Large Files For files >10MB, use streaming instead of loading into memory: ```rust theme={null} pub fn hash_large_file(path: &Path) -> io::Result { let file = File::open(path)?; let mut reader = BufReader::new(file); let mut hasher = Sha256::new(); // Stream in 8KB chunks let mut buffer = [0u8; 8192]; loop { let n = reader.read(&mut buffer)?; if n == 0 { break; } hasher.update(&buffer[..n]); } Ok(hex::encode(hasher.finalize())) } ``` ### Batch Operations Use database transactions for bulk inserts: ```rust theme={null} pub fn attach_multiple_files( files: &[PathBuf], db: &Connection, ) -> Result, String> { let tx = db.transaction()?; let mut results = vec![]; for file in files { let result = attach_file(file, &tx)?; results.push(result); } tx.commit()?; Ok(results) } ``` ## Next Steps Learn IPC communication Frontend component structure # Tauri Commands Source: https://docs.glyphformac.com/development/backend/tauri-commands IPC communication between frontend and backend ## Overview Tauri commands are the IPC (inter-process communication) layer between the React frontend and Rust backend. All commands are: * **Typed** on both sides (Rust + TypeScript) * **Async** by default * **Serialized** via JSON (serde) ## Command Flow ```mermaid theme={null} sequenceDiagram participant FE as Frontend (React) participant IPC as Tauri IPC participant CMD as Rust Command participant FS as Filesystem FE->>IPC: invoke('space_read_text', { path }) IPC->>CMD: Deserialize args CMD->>FS: Read file FS->>CMD: File contents CMD->>IPC: Serialize result IPC->>FE: Return TextFileDoc ``` ## Defining Commands ### Step 1: Implement Rust Command ```rust src-tauri/src/space_fs/read_write/text.rs theme={null} use tauri::State; use crate::space::SpaceState; #[derive(serde::Serialize)] pub struct TextFileDoc { pub rel_path: String, pub text: String, pub etag: String, pub mtime_ms: u64, } #[tauri::command] pub fn space_read_text( path: String, state: State, ) -> Result { let current = state.current.lock().unwrap(); let space = current.as_ref().ok_or("No space open")?; let abs_path = paths::join_under(&space.root, &path) .map_err(|e| format!("Invalid path: {}", e))?; let text = fs::read_to_string(&abs_path) .map_err(|e| format!("Failed to read: {}", e))?; let metadata = fs::metadata(&abs_path)?; let mtime_ms = metadata.modified()? .duration_since(UNIX_EPOCH) .unwrap() .as_millis() as u64; let etag = format!("{}-{}", mtime_ms, metadata.len()); Ok(TextFileDoc { rel_path: path, text, etag, mtime_ms, }) } ``` ### Step 2: Register in lib.rs ```rust src-tauri/src/lib.rs theme={null} .invoke_handler(tauri::generate_handler![ space_read_text, space_write_text, space_list_dir, // ... other commands ]) ``` ### Step 3: Add TypeScript Types ```typescript src/lib/tauri.ts theme={null} export interface TextFileDoc { rel_path: string; text: string; etag: string; mtime_ms: number; } interface TauriCommands { space_read_text: CommandDef<{ path: string }, TextFileDoc>; } ``` ### Step 4: Invoke from Frontend ```typescript theme={null} import { invoke } from '@/lib/tauri'; const doc = await invoke('space_read_text', { path: 'notes/example.md' }); console.log(doc.text); // File contents console.log(doc.etag); // ETag for caching ``` ## Command Categories ### Space Lifecycle Creates a new space at the given path ```typescript theme={null} const info = await invoke('space_create', { path: '/Users/me/my-space' }); // Returns: { root: '/Users/me/my-space', schema_version: 1 } ``` Opens an existing space ```typescript theme={null} const info = await invoke('space_open', { path: '/Users/me/my-space' }); ``` Returns current space path or null ```typescript theme={null} const path = await invoke('space_get_current'); // Returns: '/Users/me/my-space' or null ``` Closes the current space ```typescript theme={null} await invoke('space_close'); ``` ### File System Operations Lists files and directories ```typescript theme={null} const entries = await invoke('space_list_dir', { dir: 'notes/projects' // Optional, defaults to root }); // Returns: [{ name: 'file.md', rel_path: 'notes/projects/file.md', kind: 'file', is_markdown: true }] ``` Reads a text file with metadata ```typescript theme={null} const doc = await invoke('space_read_text', { path: 'notes/example.md' }); // Returns: { rel_path, text, etag, mtime_ms } ``` Writes a text file atomically ```typescript theme={null} const result = await invoke('space_write_text', { path: 'notes/example.md', text: '# Hello World', base_mtime_ms: doc.mtime_ms // Optional: detect conflicts }); // Returns: { etag: '...', mtime_ms: 1234567890 } ``` Creates a directory ```typescript theme={null} await invoke('space_create_dir', { path: 'notes/new-folder' }); ``` Renames/moves a file or directory ```typescript theme={null} await invoke('space_rename_path', { from_path: 'notes/old.md', to_path: 'notes/new.md' }); ``` Deletes a file or directory ```typescript theme={null} await invoke('space_delete_path', { path: 'notes/old-folder', recursive: true }); ``` ### Search & Index Rebuilds the SQLite full-text search index ```typescript theme={null} const result = await invoke('index_rebuild'); // Returns: { indexed: 1234 } ``` Full-text search across all notes ```typescript theme={null} const results = await invoke('search', { query: 'machine learning' }); // Returns: [{ id: 'notes/ml.md', title: 'ML Notes', snippet: '...', score: 0.95 }] ``` Advanced search with filters ```typescript theme={null} const results = await invoke('search_advanced', { request: { query: 'AI', tags: ['research', 'paper'], title_only: false, limit: 50 } }); ``` Lists all tags with usage counts ```typescript theme={null} const tags = await invoke('tags_list', { limit: 100 }); // Returns: [{ tag: 'research', count: 42 }, { tag: 'project', count: 18 }] ``` Finds notes linking to a given note ```typescript theme={null} const links = await invoke('backlinks', { note_id: 'notes/example.md' }); // Returns: [{ id: 'notes/other.md', title: 'Other Note', updated: '2024-03-15T10:30:00Z' }] ``` ### AI Commands Starts an AI chat conversation ```typescript theme={null} const result = await invoke('ai_chat_start', { request: { profile_id: 'openai-gpt4', messages: [ { role: 'user', content: 'Explain quantum computing' } ], mode: 'chat', context: '# Research Notes\n...', audit: true } }); // Returns: { job_id: 'abc123' } ``` Lists configured AI provider profiles ```typescript theme={null} const profiles = await invoke('ai_profiles_list'); // Returns: [{ id: 'openai', name: 'OpenAI GPT-4', provider: 'openai', model: 'gpt-4', ... }] ``` Lists available models for a provider ```typescript theme={null} const models = await invoke('ai_models_list', { profile_id: 'openai' }); // Returns: [{ id: 'gpt-4', name: 'GPT-4', context_length: 8192, ... }] ``` ### Tasks Queries tasks by bucket (inbox, today, upcoming) ```typescript theme={null} const tasks = await invoke('tasks_query', { bucket: 'today', today: '2024-03-15', limit: 100, folders: ['notes/projects'] }); // Returns: [{ task_id: '...', note_title: 'Project X', raw_text: '- [ ] Task', ... }] ``` Toggles task completion ```typescript theme={null} await invoke('task_set_checked', { task_id: 'task-abc123', checked: true }); ``` ## Error Handling ### Rust Side Always return `Result`: ```rust theme={null} #[tauri::command] pub fn risky_operation(path: String) -> Result { if path.is_empty() { return Err("Path cannot be empty".to_string()); } let contents = fs::read_to_string(&path) .map_err(|e| format!("Failed to read {}: {}", path, e))?; Ok(contents) } ``` ### Frontend Side Use try/catch with `TauriInvokeError`: ```typescript theme={null} import { invoke, TauriInvokeError } from '@/lib/tauri'; try { const result = await invoke('risky_operation', { path: '' }); } catch (err) { if (err instanceof TauriInvokeError) { console.error('Command failed:', err.message); console.error('Raw error:', err.raw); } } ``` ## State Management ### Tauri State Global state accessible to all commands: ```rust src-tauri/src/lib.rs theme={null} use tauri::Manager; pub fn run() { tauri::Builder::default() .manage(SpaceState { current: Mutex::new(None), }) .invoke_handler(tauri::generate_handler![...]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` Access in commands: ```rust theme={null} #[tauri::command] fn my_command(state: State) -> Result { let current = state.current.lock().unwrap(); let space = current.as_ref().ok_or("No space open")?; Ok(space.root.display().to_string()) } ``` ## Type Safety ### Enforcing Type Consistency The `TauriCommands` interface ensures TypeScript types match Rust: ```typescript src/lib/tauri.ts theme={null} type CommandDef = { args: Args; result: Result }; interface TauriCommands { space_read_text: CommandDef<{ path: string }, TextFileDoc>; // ^──────────────^───────────^ // Args match Rust Result matches Rust } ``` The `invoke()` helper enforces these types: ```typescript theme={null} export async function invoke( command: K, ...args: ArgsTuple ): Promise { // Implementation } ``` TypeScript will error if you: * Pass wrong argument types * Forget required arguments * Expect wrong return type ## Performance Tips ### Batch Operations Instead of N individual calls: ```typescript Bad theme={null} for (const path of paths) { const doc = await invoke('space_read_text', { path }); // Process doc... } ``` Use batch command: ```typescript Good theme={null} const docs = await invoke('space_read_texts_batch', { paths }); // Process all docs... ``` ### Streaming Large Data For large results, use events instead of return values: ```rust theme={null} use tauri::Emitter; #[tauri::command] pub fn large_operation(app: tauri::AppHandle) -> Result<(), String> { for chunk in get_large_data() { app.emit("data-chunk", &chunk)?; } Ok(()) } ``` ```typescript theme={null} import { listen } from '@tauri-apps/api/event'; const unlisten = await listen('data-chunk', (event) => { console.log('Received chunk:', event.payload); }); await invoke('large_operation'); unlisten(); ``` ## Next Steps Learn about SQLite indexing Content-addressed file storage # Building Glyph Source: https://docs.glyphformac.com/development/building Build commands and production release process ## Available Commands ### Development ```bash Frontend Dev Server theme={null} pnpm dev # Starts Vite at http://localhost:5173 # Hot module replacement enabled # Tauri commands will NOT work ``` ```bash Full Tauri App theme={null} pnpm tauri dev # Compiles Rust backend # Starts Vite dev server # Opens native desktop window # Auto-reloads on changes ``` ### Type Checking ```bash TypeScript theme={null} pnpm build # Runs: tsc && vite build # Checks types + builds frontend ``` ```bash Rust theme={null} cd src-tauri && cargo check # Type-checks Rust without building ``` ```bash Rust (with lints) theme={null} cd src-tauri && cargo clippy # Runs Clippy linter for Rust ``` ### Linting & Formatting ```bash Check All theme={null} pnpm check # Runs Biome lint + format check # Exits with error if issues found ``` ```bash Lint Only theme={null} pnpm lint # Runs Biome linter ``` ```bash Auto-format theme={null} pnpm format # Formats all files with Biome # Auto-organizes imports ``` ### Testing ```bash Run All Tests theme={null} pnpm test # Runs Vitest test suite ``` ```bash Watch Mode theme={null} pnpm test:watch # Re-runs tests on file changes ``` ```bash Single File theme={null} pnpm test -- src/lib/diff.test.ts # Runs specific test file ``` ```bash Single Test theme={null} pnpm test -- -t "test name" # Runs test matching name ``` ## Production Build ### Build Desktop App ```bash theme={null} pnpm tauri build ``` This command: Runs `tsc` to verify frontend types Bundles frontend to `src-tauri/target/release/bundle/` * Minifies JavaScript/CSS * Optimizes images * Generates source maps (optional) Compiles Rust with optimizations: ```bash theme={null} cargo build --release ``` * Full optimizations (`-O3` equivalent) * No debug symbols (smaller binary) * Takes \~2-5 minutes Generates installers for current platform ### Output Artifacts ``` src-tauri/target/release/bundle/ ├── dmg/ │ └── Glyph_0.1.10_aarch64.dmg # Installer ├── macos/ │ └── Glyph.app # Application bundle └── updater/ └── Glyph_0.1.10_aarch64.app.tar.gz # Auto-updater ``` Separate builds needed for `x86_64` (Intel) and `aarch64` (Apple Silicon) ``` src-tauri/target/release/bundle/ ├── msi/ │ └── Glyph_0.1.10_x64_en-US.msi # Installer ├── nsis/ │ └── Glyph_0.1.10_x64-setup.exe # NSIS installer └── updater/ └── Glyph_0.1.10_x64.msi.zip # Auto-updater ``` ``` src-tauri/target/release/bundle/ ├── deb/ │ └── glyph_0.1.10_amd64.deb # Debian package ├── appimage/ │ └── glyph_0.1.10_amd64.AppImage # Portable app └── updater/ └── glyph_0.1.10_amd64.AppImage.tar.gz ``` ## Pre-push Checklist Before pushing to remote, run: ```bash theme={null} pnpm check && pnpm build && cd src-tauri && cargo check ``` This verifies: * ✅ Code is formatted (Biome) * ✅ No linting errors (Biome) * ✅ TypeScript compiles * ✅ Rust compiles Add this as a Git pre-push hook: ```bash .git/hooks/pre-push theme={null} #!/bin/sh pnpm check && pnpm build && cd src-tauri && cargo check ``` Make executable: ```bash theme={null} chmod +x .git/hooks/pre-push ``` ## Build Optimization ### Rust Release Profile Configured in `src-tauri/Cargo.toml`: ```toml Cargo.toml theme={null} [profile.release] opt-level = 3 # Maximum optimization lto = true # Link-time optimization codegen-units = 1 # Single codegen unit (slower build, faster runtime) strip = true # Remove debug symbols panic = 'abort' # Smaller binary (no unwinding) ``` ### Vite Build Options Configured in `vite.config.ts`: ```typescript vite.config.ts theme={null} export default defineConfig({ build: { target: 'esnext', minify: 'esbuild', // Fast minification sourcemap: false, // Disable for smaller bundle rollupOptions: { output: { manualChunks: { // Code splitting vendor: ['react', 'react-dom'], editor: ['@tiptap/react', '@tiptap/starter-kit'] } } } } }); ``` ## Platform-Specific Builds ### macOS: Universal Binary Build for both Intel and Apple Silicon: ```bash theme={null} # Build x86_64 (Intel) rustup target add x86_64-apple-darwin pnpm tauri build -- --target x86_64-apple-darwin # Build aarch64 (Apple Silicon) rustup target add aarch64-apple-darwin pnpm tauri build -- --target aarch64-apple-darwin # Combine into universal binary lipo -create \ src-tauri/target/x86_64-apple-darwin/release/glyph \ src-tauri/target/aarch64-apple-darwin/release/glyph \ -output glyph-universal ``` ### Windows: 32-bit and 64-bit ```bash theme={null} # 64-bit (default) pnpm tauri build # 32-bit rustup target add i686-pc-windows-msvc pnpm tauri build -- --target i686-pc-windows-msvc ``` ### Linux: Multiple Distros ```bash theme={null} # Debian/Ubuntu (.deb) pnpm tauri build -- --bundles deb # AppImage (universal) pnpm tauri build -- --bundles appimage # Both pnpm tauri build -- --bundles deb,appimage ``` ## CI/CD Pipeline ### GitHub Actions Example ```yaml .github/workflows/build.yml theme={null} name: Build on: push: branches: [main] pull_request: jobs: typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 with: version: 10.28.2 - uses: actions/setup-node@v3 with: node-version: 18 cache: pnpm - run: pnpm install - run: pnpm check - run: pnpm build build-macos: runs-on: macos-latest steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v3 - uses: dtolnay/rust-toolchain@stable - run: pnpm install - run: pnpm tauri build - uses: actions/upload-artifact@v3 with: name: macos-dmg path: src-tauri/target/release/bundle/dmg/*.dmg ``` ## Versioning Version is stored in two places and **must match**: ```json package.json theme={null} { "version": "0.1.10" } ``` ```toml src-tauri/Cargo.toml theme={null} [package] version = "0.1.10" ``` If versions don't match, build will fail. Use a script to sync versions: ```bash scripts/bump-version.sh theme={null} #!/bin/bash VERSION=$1 jq ".version = \"$VERSION\"" package.json > package.json.tmp mv package.json.tmp package.json sed -i '' "s/version = .*/version = \"$VERSION\"/" src-tauri/Cargo.toml ``` Usage: `./scripts/bump-version.sh 0.1.11` ## Bundle Size Analysis ### Frontend Bundle ```bash theme={null} pnpm build # Check output in terminal: # dist/assets/index-a1b2c3.js 245.67 kB ``` ### Rust Binary Size ```bash theme={null} cd src-tauri cargo build --release ls -lh target/release/glyph # Example: 12M (macOS), 8M (Linux), 10M (Windows) ``` ### Reduce Binary Size 1. **Strip symbols** (enabled by default in release profile) 2. **Enable LTO** (enabled by default) 3. **Use `wee_alloc`** (minimal allocator): ```toml Cargo.toml theme={null} [dependencies] wee_alloc = "0.4" ``` ## Debug Builds For debugging production issues: ```bash theme={null} # Build with debug symbols pnpm tauri build -- --debug # Or modify Cargo.toml temporarily: [profile.release] strip = false debug = true ``` ## Next Steps Write and run tests Understand the codebase # React Components Source: https://docs.glyphformac.com/development/frontend/components Frontend component architecture and organization ## Component Structure Glyph's frontend is organized into logical component groups: ``` src/components/ ├── app/ # App shell & chrome ├── editor/ # TipTap markdown editor ├── ai/ # AI chat panel ├── filetree/ # File browser sidebar ├── preview/ # File preview pane ├── tasks/ # Task list views ├── database/ # Database table/board views ├── settings/ # Settings panes ├── licensing/ # License activation └── ui/ # shadcn/ui primitives ``` ## App Shell Components ### AppShell **Location**: `src/components/app/AppShell.tsx` Root layout component that orchestrates the main UI: ```typescript theme={null} export function AppShell() { return (
{/* Left sidebar: file tree, tags, etc. */} {/* Center: editor, preview, database views */} {/* Cmd+K search */} {/* ? help modal */}
); } ``` ### Sidebar **Location**: `src/components/app/Sidebar.tsx` Navigable sidebar with multiple panes: ```typescript Structure theme={null} export function Sidebar() { const { activePane } = useUIContext(); return ( ); } ``` ```typescript Switching Panes theme={null} const { setActivePane } = useUIContext(); setActivePane('files')}> ``` ### MainContent **Location**: `src/components/app/MainContent.tsx` Tab-based content area: ```typescript theme={null} export function MainContent() { const { activeFilePath, activePreviewPath } = useFileTreeContext(); const { activeViewDoc } = useViewContext(); return (
{/* File tabs */} {activeFilePath?.endsWith('.md') && ( )} {activePreviewPath && ( )} {activeViewDoc && ( )}
); } ``` ### CommandPalette **Location**: `src/components/app/CommandPalette.tsx` Cmd+K quick actions: ```typescript theme={null} import { Command } from 'cmdk'; export function CommandPalette() { const { isOpen, setIsOpen } = useUIContext(); const [query, setQuery] = useState(''); return ( New Note Settings ); } ``` ## Editor Components ### CanvasNoteInlineEditor **Location**: `src/components/editor/CanvasNoteInlineEditor.tsx` TipTap-based markdown editor: ```typescript Editor Setup theme={null} import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { Markdown } from '@tiptap/extension-markdown'; import { WikiLink } from './extensions/wikiLink'; export function CanvasNoteInlineEditor({ path }: { path: string }) { const [doc, setDoc] = useState(null); const editor = useEditor({ extensions: [ StarterKit, Markdown, WikiLink, // ... more extensions ], content: doc?.text || '', onUpdate: ({ editor }) => { debouncedSave(editor.getText()); }, }); return (
); } ``` ```typescript Auto-save theme={null} const debouncedSave = useMemo( () => debounce(async (text: string) => { try { setSaveState('saving'); await invoke('space_write_text', { path, text, base_mtime_ms: doc?.mtime_ms, }); setSaveState('saved'); } catch (err) { setSaveState('error'); toast.error('Failed to save'); } }, 500), [path, doc?.mtime_ms] ); ```
### EditorRibbon **Location**: `src/components/editor/EditorRibbon.tsx` Formatting toolbar: ```typescript theme={null} export function EditorRibbon({ editor }: { editor: Editor | null }) { if (!editor) return null; return (
editor.chain().focus().toggleBold().run()} data-active={editor.isActive('bold')} > editor.chain().focus().toggleItalic().run()} data-active={editor.isActive('italic')} > {/* ... more buttons */}
); } ``` ### NotePropertiesPanel **Location**: `src/components/editor/NotePropertiesPanel.tsx` Frontmatter editor: ```typescript theme={null} export function NotePropertiesPanel({ path }: { path: string }) { const [properties, setProperties] = useState([]); const handleAddProperty = () => { setProperties([...properties, { key: '', kind: 'text', value_text: null, value_bool: null, value_list: [], }]); }; const handleSave = async () => { const frontmatter = await invoke('note_frontmatter_render_properties', { properties, }); // Update note with new frontmatter... }; return (
{properties.map((prop, i) => ( updateProperty(i, updated)} /> ))}
); } ``` ## File Tree Components ### FileTreePane **Location**: `src/components/filetree/FileTreePane.tsx` Recursive file browser: ```typescript theme={null} export function FileTreePane() { const { rootEntries, expandedDirs } = useFileTreeContext(); const { loadDir, toggleDir, openFile } = useFileTree(/* deps */); useEffect(() => { loadDir(''); // Load root }, []); return (
{rootEntries.map(entry => ( entry.kind === 'dir' ? ( toggleDir(entry.rel_path)} /> ) : ( openFile(entry.rel_path)} /> ) ))}
); } ``` ### FileTreeDirItem **Location**: `src/components/filetree/FileTreeDirItem.tsx` Collapsible directory: ```typescript theme={null} export function FileTreeDirItem({ entry, isExpanded, onToggle, }: FileTreeDirItemProps) { const { childrenByDir } = useFileTreeContext(); const children = childrenByDir[entry.rel_path] || []; return (
{isExpanded && (
{children.map(child => ( child.kind === 'dir' ? ( ) : ( ) ))}
)}
); } ``` ## AI Components ### AIPanel **Location**: `src/components/ai/AIPanel.tsx` AI chat sidebar: ```typescript theme={null} export function AIPanel() { const { messages, sendMessage, isStreaming } = useRigChat(); return (
{/* GPT-4, Claude, etc. */} {isStreaming && }
); } ``` ### AIChatThread **Location**: `src/components/ai/AIChatThread.tsx` Message list: ```typescript theme={null} export function AIChatThread({ messages }: { messages: AiMessage[] }) { const scrollRef = useRef(null); useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages.length]); return (
{messages.map((msg, i) => (
{msg.role === 'user' ? : }
))}
); } ``` ## Database Components ### DatabasePane **Location**: `src/components/database/DatabasePane.tsx` Database view (table or board): ```typescript theme={null} export function DatabasePane({ path }: { path: string }) { const { config, rows, loading } = useDatabaseNote(path); return (
{config.view.layout === 'table' ? ( ) : ( )}
); } ``` ### DatabaseTable **Location**: `src/components/database/DatabaseTable.tsx` TanStack Table: ```typescript theme={null} import { useReactTable, getCoreRowModel } from '@tanstack/react-table'; export function DatabaseTable({ config, rows }: DatabaseTableProps) { const table = useReactTable({ data: rows, columns: config.columns.map(col => ({ id: col.id, header: col.label, cell: ({ row }) => ( handleCellUpdate(row.original, col, value)} /> ), })), getCoreRowModel: getCoreRowModel(), }); return ( {table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map(header => ( ))} ))} {table.getRowModel().rows.map(row => ( {row.getVisibleCells().map(cell => ( ))} ))}
{header.column.columnDef.header}
{cell.column.columnDef.cell(cell.getContext())}
); } ``` ## UI Primitives ### shadcn/ui Components **Location**: `src/components/ui/shadcn/` Accessible Radix UI-based components: * `Button` - Buttons with variants * `Dialog` - Modals * `Popover` - Floating menus * `DropdownMenu` - Context menus * `Input` - Text inputs * `Tabs` - Tab navigation * `Table` - Semantic tables * `ScrollArea` - Custom scrollbars ### Motion Components **Location**: `src/components/ui/animations.ts` Animated wrappers: ```typescript theme={null} import { motion } from 'motion/react'; export const MotionButton = motion.button; export const MotionPanel = motion.div; export const fadeIn = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, }; export const slideIn = { initial: { x: -20, opacity: 0 }, animate: { x: 0, opacity: 1 }, exit: { x: 20, opacity: 0 }, }; ``` Usage: ```typescript theme={null} Content appears with fade ``` ## Component Patterns ### Compound Components ```typescript theme={null} // Parent manages state, children consume via context export function Accordion({ children }) { const [openId, setOpenId] = useState(null); return ( {children} ); } Accordion.Item = function AccordionItem({ id, children }) { const { openId, setOpenId } = useAccordionContext(); const isOpen = openId === id; return (
); }; ``` ### Render Props ```typescript theme={null} interface FileListProps { files: FsEntry[]; renderFile: (file: FsEntry) => ReactNode; } export function FileList({ files, renderFile }: FileListProps) { return (
{files.map(file => (
{renderFile(file)}
))}
); } // Usage (
{file.name}
)} /> ``` ### Custom Hooks as Logic ```typescript theme={null} function useFileTreeItem(entry: FsEntry) { const { openFile, renameFile, deleteFile } = useFileTree(/* deps */); const [isRenaming, setIsRenaming] = useState(false); const handleRename = async (newName: string) => { await renameFile(entry.rel_path, newName); setIsRenaming(false); }; return { isRenaming, startRename: () => setIsRenaming(true), handleRename, handleDelete: () => deleteFile(entry.rel_path), handleOpen: () => openFile(entry.rel_path), }; } ``` ## Next Steps React Context state management Custom React hooks # React Contexts Source: https://docs.glyphformac.com/development/frontend/contexts State management via React Context ## Overview Glyph uses **React Context** for all global state management. No Redux, no Zustand—just plain React. Contexts are layered: `SpaceContext` wraps `FileTreeContext` wraps `ViewContext`, etc. ## Context Hierarchy ```typescript src/main.tsx theme={null} function App() { return ( {/* Space lifecycle */} {/* Files, tags, active file */} {/* Active view document */} {/* Sidebar, search state */} {/* TipTap editor instance */} ); } ``` ## SpaceContext **Location**: `src/contexts/SpaceContext.tsx` Manages space lifecycle (create, open, close). ### State Shape ```typescript theme={null} interface SpaceContextValue { // App metadata info: AppInfo | null; // App name, version // Current space spacePath: string | null; // '/Users/me/my-space' spaceSchemaVersion: number | null; // 1 // History lastSpacePath: string | null; // For "Continue" button recentSpaces: string[]; // Up to 20 recent paths // Index state isIndexing: boolean; // Index rebuild in progress // Lifecycle settingsLoaded: boolean; // Settings loaded from disk error: string; // Error message setError: (error: string) => void; // Actions onOpenSpace: () => Promise; onOpenSpaceAtPath: (path: string) => Promise; onContinueLastSpace: () => Promise; onCreateSpace: () => Promise; closeSpace: () => Promise; startIndexRebuild: () => Promise; } ``` ### Usage ```typescript Opening a Space theme={null} import { useSpace } from '@/contexts/SpaceContext'; function WelcomeScreen() { const { onOpenSpace, onContinueLastSpace, lastSpacePath } = useSpace(); return (
{lastSpacePath && ( )}
); } ``` ```typescript Checking Space State theme={null} function Sidebar() { const { spacePath, isIndexing } = useSpace(); if (!spacePath) { return ; } return (
{isIndexing && }
); } ```
## FileTreeContext **Location**: `src/contexts/FileTreeContext.tsx` Manages file browser state and tag index. ### State Shape ```typescript theme={null} interface FileTreeContextValue { // File tree data rootEntries: FsEntry[]; // Files/dirs at root childrenByDir: Record; // Cached children by dir path expandedDirs: Set; // Which dirs are expanded // Updaters (for hooks to modify state) updateRootEntries: (next: FsEntry[] | ((prev: FsEntry[]) => FsEntry[])) => void; updateChildrenByDir: (next: ...) => void; updateExpandedDirs: (next: Set | ((prev: Set) => Set)) => void; // Active file activeFilePath: string | null; // 'notes/example.md' setActiveFilePath: (path: string | null) => void; // Derived state (computed from activeFilePath) activeNoteId: string | null; // Same as activeFilePath if .md activeNoteTitle: string | null; // Filename without extension // Tag index tags: TagCount[]; // [{ tag: 'research', count: 42 }] tagsError: string; refreshTags: () => Promise; } ``` ### Usage ```typescript Reading File Tree theme={null} import { useFileTreeContext } from '@/contexts/FileTreeContext'; function FileTreePane() { const { rootEntries, expandedDirs } = useFileTreeContext(); return (
{rootEntries.map(entry => ( ))}
); } ``` ```typescript Updating State theme={null} function useFileTree() { const { updateExpandedDirs } = useFileTreeContext(); const toggleDir = (dirPath: string) => { updateExpandedDirs(prev => { const next = new Set(prev); if (next.has(dirPath)) { next.delete(dirPath); } else { next.add(dirPath); } return next; }); }; return { toggleDir }; } ```
## ViewContext **Location**: `src/contexts/ViewContext.tsx` Manages "view documents" (folder, tag, search, database views). ### State Shape ```typescript theme={null} interface ViewContextValue { activeViewDoc: ViewDoc | null; // Current view (folder, tag, etc.) setActiveViewDoc: (doc: ViewDoc | null) => void; isLoadingView: boolean; viewError: string; } type ViewDoc = | FolderViewDoc | TagViewDoc | SearchViewDoc | DatabaseViewDoc; interface FolderViewDoc { type: 'folder'; dir: string; files: FsEntry[]; subfolders: FolderViewFolder[]; note_previews: ViewNotePreview[]; } ``` ### Usage ```typescript theme={null} import { useViewContext } from '@/contexts/ViewContext'; function MainContent() { const { activeViewDoc } = useViewContext(); if (activeViewDoc?.type === 'folder') { return ; } if (activeViewDoc?.type === 'database') { return ; } return ; } ``` ## UIContext **Location**: `src/contexts/UIContext.tsx` Manages UI chrome state (sidebar, search, modals). ### State Shape ```typescript theme={null} interface UIContextValue { // Sidebar activePane: 'files' | 'tags' | 'ai' | 'tasks'; setActivePane: (pane: 'files' | 'tags' | 'ai' | 'tasks') => void; isSidebarCollapsed: boolean; toggleSidebar: () => void; // Command palette (Cmd+K) isCommandPaletteOpen: boolean; openCommandPalette: () => void; closeCommandPalette: () => void; // Search searchQuery: string; setSearchQuery: (query: string) => void; // Preview pane activePreviewPath: string | null; setActivePreviewPath: (path: string | null) => void; } ``` ### Usage ```typescript Sidebar Panes theme={null} function SidebarHeader() { const { activePane, setActivePane } = useUIContext(); return (
setActivePane('files')} data-active={activePane === 'files'} > setActivePane('ai')} data-active={activePane === 'ai'} >
); } ``` ```typescript Command Palette theme={null} function App() { const { isCommandPaletteOpen, openCommandPalette } = useUIContext(); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openCommandPalette(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [openCommandPalette]); return ( <> {isCommandPaletteOpen && } ); } ```
## EditorContext **Location**: `src/contexts/EditorContext.tsx` Manages TipTap editor instance. ### State Shape ```typescript theme={null} import type { Editor } from '@tiptap/react'; interface EditorContextValue { editor: Editor | null; // TipTap editor instance setEditor: (editor: Editor | null) => void; isEditing: boolean; // Focus state saveState: 'saved' | 'saving' | 'unsaved' | 'error'; setSaveState: (state: 'saved' | 'saving' | 'unsaved' | 'error') => void; } ``` ### Usage ```typescript Creating Editor theme={null} import { useEditor } from '@tiptap/react'; import { useEditorContext } from '@/contexts/EditorContext'; function MarkdownEditorPane() { const { setEditor, setSaveState } = useEditorContext(); const editor = useEditor({ extensions: [/* ... */], onUpdate: () => { setSaveState('unsaved'); debouncedSave(); }, onFocus: () => setIsEditing(true), onBlur: () => setIsEditing(false), }); useEffect(() => { setEditor(editor); return () => setEditor(null); }, [editor, setEditor]); return ; } ``` ```typescript Using Editor theme={null} function EditorRibbon() { const { editor } = useEditorContext(); if (!editor) return null; return (
); } ```
## Custom Context Pattern All contexts follow this pattern: ```typescript theme={null} interface MyContextValue { data: string; setData: (data: string) => void; } ``` ```typescript theme={null} const MyContext = createContext(null); ``` ```typescript theme={null} export function MyProvider({ children }: { children: ReactNode }) { const [data, setData] = useState(''); const value = useMemo( () => ({ data, setData }), [data] ); return ( {children} ); } ``` ```typescript theme={null} export function useMyContext(): MyContextValue { const ctx = useContext(MyContext); if (!ctx) { throw new Error('useMyContext must be used within MyProvider'); } return ctx; } ``` ## Performance Optimization ### Split Contexts Instead of one giant context: ```typescript Bad theme={null} interface AppContextValue { user: User; theme: Theme; files: File[]; // ... 20 more fields } // Every component re-renders when ANY field changes! ``` Use multiple small contexts: ```typescript Good theme={null} {/* Components only re-render when their context changes */} ``` ### Memoize Context Value Always memoize the context value: ```typescript theme={null} const value = useMemo( () => ({ data, setData }), [data] // Only recompute when data changes ); ``` Without `useMemo`, context consumers re-render on every provider render. ### Selector Pattern For large contexts, expose selectors: ```typescript theme={null} interface FileTreeContextValue { // Instead of exposing entire state: // state: { rootEntries, childrenByDir, ... } // Expose specific selectors: useRootEntries: () => FsEntry[]; useChildrenByDir: () => Record; useExpandedDirs: () => Set; } // Components only subscribe to what they use function FileList() { const rootEntries = useFileTreeContext().useRootEntries(); // Only re-renders when rootEntries changes } ``` ## Next Steps Custom React hooks Component architecture # React Hooks Source: https://docs.glyphformac.com/development/frontend/hooks Custom hooks for business logic ## Overview Glyph uses custom hooks to **separate business logic from UI**. Hooks handle: * File tree operations (load, rename, delete) * Editor state (save, auto-save, conflict detection) * Search (full-text, advanced filters) * AI chat (streaming, tool calls) * View loading (folder, tag, database views) Hooks follow the "hooks as logic, components as UI" pattern. Components should be thin wrappers around hooks. ## File Tree Hooks ### useFileTree **Location**: `src/hooks/useFileTree.ts` Manages file tree operations (load, toggle, open). ```typescript Interface theme={null} interface UseFileTreeResult { loadDir: (dirPath: string, force?: boolean) => Promise; toggleDir: (dirPath: string) => void; openFile: (relPath: string) => Promise; openMarkdownFile: (relPath: string) => Promise; openNonMarkdownExternally: (relPath: string) => Promise; // CRUD operations (from useFileTreeCRUD) onNewFile: () => Promise; onNewFileInDir: (dirPath: string) => Promise; onNewFolderInDir: (dirPath: string) => Promise; onRenameDir: (path: string, nextName: string) => Promise; onDeletePath: (path: string, kind: 'dir' | 'file') => Promise; onMovePath: (fromPath: string, toDirPath: string) => Promise; } ``` ```typescript Usage theme={null} import { useFileTree } from '@/hooks/useFileTree'; function FileTreePane() { const { spacePath } = useSpace(); const { updateChildrenByDir, expandedDirs, ... } = useFileTreeContext(); const { setActiveFilePath, setActivePreviewPath, activeFilePath } = useUIContext(); const fileTree = useFileTree({ spacePath, updateChildrenByDir, updateExpandedDirs, setActiveFilePath, setActivePreviewPath, activeFilePath, // ... other deps }); return (
); } ```
### useFileTreeCRUD **Location**: `src/hooks/useFileTreeCRUD.ts` Create, rename, delete operations. ```typescript src/hooks/useFileTreeCRUD.ts theme={null} export function useFileTreeCRUD(deps: UseFileTreeCRUDDeps) { const onNewFile = useCallback(async () => { const dirPath = deps.getActiveFolderDir() || ''; const name = prompt('File name:'); if (!name) return; const path = dirPath ? `${dirPath}/${name}` : name; try { await invoke('space_open_or_create_text', { path, text: '# ' + name.replace(/\.md$/, '') }); await deps.loadDir(dirPath, true); // Refresh deps.setActiveFilePath(path); } catch (err) { deps.setError(extractErrorMessage(err)); } }, [deps]); const onDeletePath = useCallback(async ( path: string, kind: 'dir' | 'file' ): Promise => { const confirmed = confirm(`Delete ${kind} "${path}"?`); if (!confirmed) return false; try { await invoke('space_delete_path', { path, recursive: kind === 'dir' }); // Refresh parent directory const parentPath = parentDir(path); await deps.loadDir(parentPath, true); // Clear active file if deleted if (deps.activeFilePath === path) { deps.setActiveFilePath(null); } return true; } catch (err) { deps.setError(extractErrorMessage(err)); return false; } }, [deps]); return { onNewFile, onNewFileInDir, onNewFolderInDir, onRenameDir, onDeletePath, onMovePath, }; } ``` ## Search Hooks ### useSearch **Location**: `src/hooks/useSearch.ts` Debounced search with result caching. ```typescript theme={null} import { useCallback, useEffect, useState, useMemo } from 'react'; import { invoke } from '@/lib/tauri'; import type { SearchResult } from '@/lib/tauri'; export function useSearch(query: string, debounceMs = 300) { const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); // Debounce query const debouncedQuery = useDebounce(query, debounceMs); useEffect(() => { if (!debouncedQuery) { setResults([]); return; } let cancelled = false; (async () => { setLoading(true); setError(''); try { const searchResults = await invoke('search', { query: debouncedQuery }); if (!cancelled) { setResults(searchResults); } } catch (err) { if (!cancelled) { setError(extractErrorMessage(err)); } } finally { if (!cancelled) { setLoading(false); } } })(); return () => { cancelled = true; }; }, [debouncedQuery]); return { results, loading, error }; } function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } ``` ## View Loading Hooks ### useViewLoader **Location**: `src/hooks/useViewLoader.ts` Loads and builds view documents (folder, tag, search, database). ```typescript theme={null} export function useViewLoader() { const { setActiveViewDoc, setIsLoadingView, setViewError } = useViewContext(); const loadFolderView = useCallback(async (dir: string) => { setIsLoadingView(true); setViewError(''); try { const data = await invoke('space_folder_view_data', { dir, limit: 100, recent_limit: 10 }); setActiveViewDoc({ type: 'folder', dir, files: data.files, subfolders: data.subfolders, note_previews: data.note_previews, }); } catch (err) { setViewError(extractErrorMessage(err)); } finally { setIsLoadingView(false); } }, [setActiveViewDoc, setIsLoadingView, setViewError]); const loadTagView = useCallback(async (tag: string) => { setIsLoadingView(true); try { const previews = await invoke('tag_view_data', { tag, limit: 100 }); setActiveViewDoc({ type: 'tag', tag, note_previews: previews, }); } catch (err) { setViewError(extractErrorMessage(err)); } finally { setIsLoadingView(false); } }, [setActiveViewDoc, setIsLoadingView, setViewError]); return { loadFolderView, loadTagView, loadSearchView, loadDatabaseView, }; } ``` ## Editor Hooks ### useNoteEditor **Location**: `src/components/editor/hooks/useNoteEditor.ts` Manages TipTap editor with auto-save and conflict detection. ```typescript theme={null} import { useEditor } from '@tiptap/react'; import { useMemo, useCallback, useEffect, useState } from 'react'; import { debounce } from '@/lib/utils'; export function useNoteEditor(path: string) { const [doc, setDoc] = useState(null); const [saveState, setSaveState] = useState<'saved' | 'saving' | 'unsaved'>('saved'); // Load note useEffect(() => { let cancelled = false; (async () => { try { const loaded = await invoke('space_read_text', { path }); if (!cancelled) setDoc(loaded); } catch (err) { toast.error('Failed to load note'); } })(); return () => { cancelled = true; }; }, [path]); // Auto-save handler const handleSave = useCallback(async (text: string) => { if (!doc) return; setSaveState('saving'); try { const result = await invoke('space_write_text', { path, text, base_mtime_ms: doc.mtime_ms, // Conflict detection }); setDoc(prev => prev ? { ...prev, etag: result.etag, mtime_ms: result.mtime_ms } : null); setSaveState('saved'); } catch (err) { if (err.message.includes('conflict')) { toast.error('File was modified externally. Reload to see changes.'); } else { toast.error('Failed to save'); } setSaveState('unsaved'); } }, [path, doc]); const debouncedSave = useMemo( () => debounce(handleSave, 500), [handleSave] ); // TipTap editor const editor = useEditor({ extensions: [/* ... */], content: doc?.text || '', onUpdate: ({ editor }) => { setSaveState('unsaved'); debouncedSave(editor.getText()); }, }); return { editor, doc, saveState, }; } ``` ## AI Hooks ### useRigChat **Location**: `src/components/ai/hooks/useRigChat.ts` Manages streaming AI chat with tool calls. ```typescript theme={null} import { useState, useCallback } from 'react'; import { listen } from '@tauri-apps/api/event'; export function useRigChat() { const [messages, setMessages] = useState([]); const [isStreaming, setIsStreaming] = useState(false); const [currentJobId, setCurrentJobId] = useState(null); const sendMessage = useCallback(async (content: string) => { const userMessage: AiMessage = { role: 'user', content }; setMessages(prev => [...prev, userMessage]); setIsStreaming(true); try { const { job_id } = await invoke('ai_chat_start', { request: { profile_id: activeProfile.id, messages: [...messages, userMessage], mode: 'chat', } }); setCurrentJobId(job_id); // Listen for streaming chunks const unlisten = await listen<{ delta: string }>( `ai_stream_${job_id}`, (event) => { setMessages(prev => { const last = prev[prev.length - 1]; if (last?.role === 'assistant') { return [ ...prev.slice(0, -1), { ...last, content: last.content + event.payload.delta } ]; } else { return [ ...prev, { role: 'assistant', content: event.payload.delta } ]; } }); } ); // Wait for completion await listen<{ job_id: string }>( `ai_complete_${job_id}`, () => { setIsStreaming(false); setCurrentJobId(null); unlisten(); } ); } catch (err) { toast.error('AI request failed'); setIsStreaming(false); } }, [messages, activeProfile]); const cancelStream = useCallback(async () => { if (!currentJobId) return; try { await invoke('ai_chat_cancel', { job_id: currentJobId }); setIsStreaming(false); setCurrentJobId(null); } catch (err) { console.error('Failed to cancel:', err); } }, [currentJobId]); return { messages, isStreaming, sendMessage, cancelStream, }; } ``` ## Database Hooks ### useDatabaseTable **Location**: `src/hooks/database/useDatabaseTable.ts` Manages database view (table/board) state. ```typescript theme={null} import { useState, useEffect, useCallback } from 'react'; import type { DatabaseConfig, DatabaseRow } from '@/lib/tauri'; export function useDatabaseTable(path: string) { const [config, setConfig] = useState(null); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); // Load database useEffect(() => { let cancelled = false; (async () => { setLoading(true); try { const data = await invoke('database_load', { path, limit: 500 }); if (!cancelled) { setConfig(data.config); setRows(data.rows); } } catch (err) { toast.error('Failed to load database'); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [path]); // Update cell const updateCell = useCallback(async ( row: DatabaseRow, column: DatabaseColumn, value: DatabaseCellValue ) => { try { const updatedRow = await invoke('database_update_cell', { note_path: row.note_path, column, value, }); setRows(prev => prev.map(r => r.note_path === row.note_path ? updatedRow : r )); } catch (err) { toast.error('Failed to update cell'); } }, []); // Create row const createRow = useCallback(async (title?: string) => { if (!config) return; try { const result = await invoke('database_create_row', { database_path: path, title, }); setRows(prev => [result.row, ...prev]); // Open new note // ... } catch (err) { toast.error('Failed to create row'); } }, [path, config]); return { config, rows, loading, updateCell, createRow, }; } ``` ## Hook Patterns ### Dependencies Object Pattern Instead of 20 individual parameters: ```typescript Good theme={null} interface UseFileTreeDeps { spacePath: string | null; updateChildrenByDir: (...) => void; setActiveFilePath: (path: string | null) => void; // ... more } export function useFileTree(deps: UseFileTreeDeps) { // Use deps.spacePath, deps.updateChildrenByDir, etc. } ``` ### Async Hook Pattern For hooks that load data: ```typescript theme={null} export function useAsyncData(fetchFn: () => Promise) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; (async () => { try { const result = await fetchFn(); if (!cancelled) setData(result); } catch (err) { if (!cancelled) setError(err as Error); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [fetchFn]); return { data, loading, error }; } ``` ## Next Steps Component architecture State management # Development Setup Source: https://docs.glyphformac.com/development/setup Set up your development environment for Glyph ## Prerequisites ### Required Tools Download from [nodejs.org](https://nodejs.org/) or use a version manager: ```bash macOS/Linux (nvm) theme={null} nvm install 18 nvm use 18 ``` ```bash Windows (nvm-windows) theme={null} nvm install 18 nvm use 18 ``` Fast, disk-efficient package manager: ```bash theme={null} npm install -g pnpm@10.28.2 ``` Verify installation: ```bash theme={null} pnpm --version # Should output: 10.28.2 ``` Install via [rustup](https://rustup.rs/): ```bash macOS/Linux theme={null} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` ```powershell Windows theme={null} # Download and run rustup-init.exe from https://rustup.rs/ ``` Verify installation: ```bash theme={null} rustc --version cargo --version ``` ```bash theme={null} # Install Xcode Command Line Tools xcode-select --install ``` ```bash theme={null} sudo apt update sudo apt install libwebkit2gtk-4.1-dev \ build-essential \ curl \ wget \ file \ libssl-dev \ libayatana-appindicator3-dev \ librsvg2-dev ``` Install: * [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) * [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) (usually pre-installed on Windows 11) ## Clone Repository ```bash theme={null} git clone https://github.com/YourOrg/Glyph.git cd Glyph ``` ## Install Dependencies ```bash theme={null} pnpm install ``` This installs all packages from `package.json`. Rust dependencies are automatically downloaded during first build. To verify Rust setup: ```bash theme={null} cd src-tauri cargo check ``` ## Editor Setup ### VS Code (Recommended) Install recommended extensions: ```json .vscode/extensions.json theme={null} { "recommendations": [ "rust-lang.rust-analyzer", // Rust language support "tauri-apps.tauri-vscode", // Tauri tooling "biomejs.biome", // Linting + formatting "bradlc.vscode-tailwindcss" // Tailwind IntelliSense ] } ``` ### Settings ```json .vscode/settings.json theme={null} { "editor.defaultFormatter": "biomejs.biome", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "quickfix.biome": "explicit", "source.organizeImports.biome": "explicit" }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", "editor.formatOnSave": true }, "rust-analyzer.check.command": "clippy" } ``` ## Environment Variables ### Optional: AI Provider Keys Create `.env` in project root for development API keys: ```bash .env theme={null} # OpenAI OPENAI_API_KEY=sk-... # Anthropic ANTHROPIC_API_KEY=sk-ant-... # Google Gemini GEMINI_API_KEY=... ``` These are **optional**. Glyph stores API keys in the user's space via the settings UI. `.env` is only for testing during development. ## Verify Setup Run these commands to verify everything is working: ```bash theme={null} pnpm build # Should complete without errors ``` ```bash theme={null} cd src-tauri cargo check # Should complete without errors ``` ```bash theme={null} pnpm check # Should pass all Biome checks ``` ```bash theme={null} pnpm test # Should pass all Vitest tests ``` ## Running the App ### Development Mode ```bash theme={null} pnpm tauri dev ``` This: * Starts Vite dev server with HMR * Compiles Rust backend * Opens native desktop window * Auto-reloads on file changes ```bash theme={null} pnpm dev ``` Opens Vite dev server at `http://localhost:5173`. Tauri commands will **not work** in this mode. Use for UI-only development. ### Hot Reload Behavior * **Frontend changes** (TypeScript/React/CSS): Instant HMR * **Rust changes**: Full recompile (\~5-30s depending on changes) ## Troubleshooting ### "pnpm: command not found" ```bash theme={null} npm install -g pnpm ``` ### "rustc: command not found" Restart terminal after installing Rust, or run: ```bash theme={null} source $HOME/.cargo/env ``` ### Tauri dev fails with "webkit2gtk not found" (Linux) ```bash theme={null} sudo apt install libwebkit2gtk-4.1-dev ``` ### "error: linker `cc` not found" (Linux) ```bash theme={null} sudo apt install build-essential ``` ### macOS: "xcrun: error: invalid active developer path" ```bash theme={null} xcode-select --install ``` ### Windows: "error: the `cargo` binary is missing" Ensure `%USERPROFILE%\.cargo\bin` is in your PATH. Restart terminal after installing Rust. ### pnpm install fails with EACCES ```bash theme={null} # Fix npm global permissions sudo chown -R $(whoami) ~/.npm sudo chown -R $(whoami) /usr/local/lib/node_modules ``` ### Biome errors in VS Code Ensure Biome extension is installed and enabled: ```bash theme={null} code --install-extension biomejs.biome ``` ## Next Steps Learn how to build production releases Run tests and write new ones Understand the codebase structure Learn IPC communication # Space System Source: https://docs.glyphformac.com/development/space-system Understanding Glyph's space-based architecture ## What is a Space? A **space** is Glyph's fundamental organizational unit. Each space is a directory on the user's filesystem containing: * **Notes** - Markdown files with YAML frontmatter * **Assets** - Content-addressed files (images, PDFs, etc.) * **Cache** - Derived data (link previews, thumbnails) * **Metadata** - Schema version and configuration Spaces are portable. You can move a space folder anywhere, sync via Dropbox/iCloud, or manage it with Git. ## Directory Structure ``` my-space/ ├── notes/ # User-created markdown files │ ├── daily/ # Daily notes (optional) │ ├── projects/ # Project notes │ └── meeting-notes.md │ ├── assets/ # Content-addressed storage │ ├── a1b2c3...xyz.png # SHA256 hash as filename │ └── f4e5d6...abc.pdf │ ├── cache/ # Temporary/derived data │ ├── links/ # Link preview metadata │ └── images/ # Cached external images │ ├── .glyph/ # App-managed data (not in space root) │ ├── index.db # SQLite FTS + tags + links │ ├── ai_history.db # AI chat conversations │ ├── profiles.json # AI provider configs │ └── settings.json # Space-specific settings │ └── space.json # Schema version marker ``` The `.glyph/` folder is **derived data**. It can be safely deleted and will regenerate on next space open. Do not sync it. ## Space Lifecycle ### Creating a Space User chooses an empty or existing folder via native file picker ```typescript src/contexts/SpaceContext.tsx theme={null} const onCreateSpace = async () => { const { open } = await import('@tauri-apps/plugin-dialog'); const selection = await open({ directory: true }); await invoke('space_create', { path: selection }); }; ``` Rust backend creates directories and schema marker ```rust src-tauri/src/space/commands.rs theme={null} pub fn space_create(path: String) -> Result { fs::create_dir_all(path.join("notes"))?; fs::create_dir_all(path.join("assets"))?; fs::create_dir_all(path.join("cache"))?; let schema = SpaceSchema { version: CURRENT_SCHEMA_VERSION }; write_atomic(path.join("space.json"), serde_json::to_vec(&schema)?)?; Ok(SpaceInfo { root: path, schema_version: CURRENT_SCHEMA_VERSION }) } ``` SpaceContext tracks current space path ```typescript theme={null} setSpacePath(spaceInfo.root); setSpaceSchemaVersion(spaceInfo.schema_version); await setCurrentSpacePath(spaceInfo.root); // Persist to settings ``` ### Opening a Space Ensure space is compatible with app version ```rust theme={null} const CURRENT_SCHEMA_VERSION: u32 = 1; if space_schema.version != CURRENT_SCHEMA_VERSION { return Err(format!("Incompatible space version: {}", space_schema.version)); } ``` Create or open `.glyph/index.db` ```rust src-tauri/src/index/db.rs theme={null} pub fn open_db(glyph_dir: &Path) -> Result { let db = Connection::open(glyph_dir.join("index.db"))?; schema::ensure_schema(&db)?; // Create FTS tables Ok(db) } ``` Monitor `notes/` directory for changes ```rust src-tauri/src/space/watcher.rs theme={null} let watcher = notify::recommended_watcher(move |event| { if let Ok(Event { kind: EventKind::Modify(_), paths, .. }) = event { for path in paths { indexer::reindex_file(&path)?; } } })?; watcher.watch(&space_root.join("notes"), RecursiveMode::Recursive)?; ``` Scan all notes and populate SQLite FTS ```typescript src/contexts/SpaceContext.tsx theme={null} await invoke('index_rebuild'); // Async, non-blocking ``` ### Closing a Space ```typescript src/contexts/SpaceContext.tsx theme={null} const closeSpace = async () => { await invoke('space_close'); // Stop watcher, close DB await clearCurrentSpacePath(); // Clear from settings setSpacePath(null); setSpaceSchemaVersion(null); }; ``` ## Space.json Schema The `space.json` file marks a directory as a Glyph space and tracks schema version. ```json space.json theme={null} { "version": 1 } ``` Schema version. Current version is **1**. If this doesn't match `CURRENT_SCHEMA_VERSION` in Rust code, the space cannot be opened. ## Content-Addressed Storage Assets (images, PDFs, etc.) are stored by **SHA256 hash** to deduplicate files. File is selected via file picker ```typescript theme={null} const result = await invoke('note_attach_file', { note_id: 'notes/example.md', source_path: '/Users/me/Downloads/diagram.png' }); // Returns: { asset_rel_path: 'assets/a1b2...xyz.png', markdown: '![](../assets/a1b2...xyz.png)' } ``` SHA256 hash of file contents determines storage path ```rust src-tauri/src/notes/attachments.rs theme={null} let mut hasher = Sha256::new(); io::copy(&mut file, &mut hasher)?; let hash = hex::encode(hasher.finalize()); let asset_name = format!("{}.{}", hash, extension); ``` Only copy file if hash doesn't already exist ```rust theme={null} let dest = space_root.join("assets").join(&asset_name); if !dest.exists() { fs::copy(&source_path, &dest)?; } ``` Generate relative markdown link ```rust theme={null} let rel_path = format!("../assets/{}", asset_name); Ok(AttachmentResult { asset_rel_path: format!("assets/{}", asset_name), markdown: format!("![]({})", rel_path) }) ``` ### Benefits * **Deduplication** - Same image used in 10 notes = 1 file on disk * **Integrity** - Hash mismatch = corrupted file * **Immutability** - Content can't change without changing hash ## State Management ### Backend State (`src-tauri/src/space/state.rs`) ```rust theme={null} use std::sync::Mutex; pub struct SpaceState { pub current: Mutex>, } pub struct CurrentSpace { pub root: PathBuf, pub schema_version: u32, pub db: Connection, pub watcher: RecommendedWatcher, } ``` Accessed via Tauri's state management: ```rust theme={null} #[tauri::command] fn space_get_current(state: State) -> Result, String> { let current = state.current.lock().unwrap(); Ok(current.as_ref().map(|c| c.root.display().to_string())) } ``` ### Frontend State (`src/contexts/SpaceContext.tsx`) ```typescript State Shape theme={null} interface SpaceContextValue { spacePath: string | null; // Current space root path spaceSchemaVersion: number | null; // Schema version lastSpacePath: string | null; // Last opened space (for "Continue") recentSpaces: string[]; // Recent space paths (max 20) isIndexing: boolean; // Index rebuild in progress settingsLoaded: boolean; // Settings loaded from disk } ``` ```typescript Actions theme={null} // User-triggered actions onOpenSpace: () => Promise; // Show folder picker onOpenSpaceAtPath: (path: string) => Promise; // Open specific path onContinueLastSpace: () => Promise; // Reopen last space onCreateSpace: () => Promise; // Create new space closeSpace: () => Promise; // Close current space startIndexRebuild: () => Promise; // Manual index rebuild ``` ## Recent Spaces Glyph tracks up to 20 recently opened spaces, stored in Tauri's persistent store: ```typescript src/lib/settings.ts theme={null} import { Store } from '@tauri-apps/plugin-store'; const store = new Store('settings.json'); export async function setCurrentSpacePath(path: string) { await store.set('currentSpacePath', path); // Update recent spaces const recent = (await store.get('recentSpaces')) || []; const updated = [path, ...recent.filter(p => p !== path)].slice(0, 20); await store.set('recentSpaces', updated); await store.save(); } ``` ## Path Safety ### Preventing Path Traversal All user-provided paths are validated to prevent traversal attacks: ```rust src-tauri/src/paths.rs theme={null} pub fn join_under(base: &Path, rel: &str) -> Result { let normalized = PathBuf::from(rel) .components() .filter(|c| !matches!(c, Component::ParentDir)) .collect::(); let joined = base.join(&normalized); if !joined.starts_with(base) { return Err("Path traversal detected".to_string()); } Ok(joined) } ``` Usage: ```rust src-tauri/src/space_fs/read_write/text.rs theme={null} #[tauri::command] pub fn space_read_text( path: String, state: State ) -> Result { let current = state.current.lock().unwrap(); let space = current.as_ref().ok_or("No space open")?; // Safe path join - rejects "../../../etc/passwd" let abs_path = paths::join_under(&space.root, &path)?; let text = fs::read_to_string(abs_path)?; // ... } ``` ## Schema Migration Glyph uses a **hard cutover migration policy**. When schema version changes: * Old app versions **cannot** open new spaces * New app versions **cannot** open old spaces * Users must export data and re-import This is intentional to keep the codebase simple. ### Version Check ```rust src-tauri/src/space/commands.rs theme={null} const CURRENT_SCHEMA_VERSION: u32 = 1; pub fn space_open(path: String) -> Result { let schema_path = PathBuf::from(&path).join("space.json"); let schema: SpaceSchema = serde_json::from_str(&fs::read_to_string(schema_path)?)?; if schema.version != CURRENT_SCHEMA_VERSION { return Err(format!( "Space schema version {} is not compatible with app version {} (requires version {})", schema.version, env!("CARGO_PKG_VERSION"), CURRENT_SCHEMA_VERSION )); } // Continue with space open... } ``` # Technology Stack Source: https://docs.glyphformac.com/development/tech-stack Core technologies and dependencies used in Glyph ## Frontend Stack ### Core Framework UI framework with modern hooks and concurrent features Type-safe JavaScript with strict mode enabled Build tool with fast HMR and optimized production builds ### UI & Styling Utility-first CSS framework with custom design tokens Accessible headless UI primitives (Dialog, Popover, DropdownMenu, etc.) Animation library for smooth transitions (successor to Framer Motion) Pre-built accessible components built on Radix UI + Tailwind ### Editor & Rich Text Headless rich text editor framework * `@tiptap/react` - React integration * `@tiptap/markdown` - Markdown parsing/serialization * `@tiptap/starter-kit` - Common extensions * `@tiptap/extension-*` - Task lists, tables, links, images ProseMirror core (powers TipTap) ### Data & Forms Headless table library for database views Performant form validation TypeScript-first schema validation Zod integration for react-hook-form ### Canvas & Visualization Node-based canvas editor (formerly React Flow) ### Utilities Command palette component (Command+K) Toast notifications Dark mode theme switching Type-safe CSS class variants Conditional class names + Tailwind class merging Resizable sidebar/panel layouts ## Backend Stack ### Core Framework Rust-based framework for building desktop apps * `tauri` - Core runtime * `@tauri-apps/api` - JavaScript bindings * `@tauri-apps/cli` - Build tooling Systems programming language (see `Cargo.toml`) ### Tauri Plugins Native file picker dialogs Open files/URLs in default system apps Persistent key-value settings storage Auto-update functionality System notifications Process management for Codex app-server ### Serialization & Data Rust serialization framework JSON serialization for IPC and file storage YAML frontmatter parsing SQLite bindings with embedded SQLite ### Filesystem & I/O Cross-platform filesystem watcher SHA256 hashing for content-addressed storage Hex encoding for hash strings Base64 encoding for binary data URLs ### Networking HTTP client for link preview fetching * Uses `rustls` instead of OpenSSL * `blocking` - Synchronous API * `json` - JSON body support * `stream` - Streaming responses URL parsing and validation ### AI & LLM Multi-provider LLM framework * OpenAI, Anthropic, Gemini, Ollama support * Tool calling & structured outputs JSON Schema generation for AI tool definitions ### Utilities UUID generation for note IDs Date/time handling (used for daily notes) Regular expressions for parsing Structured logging Log filtering and formatting Async runtime for AI streaming Async utilities Additional Tokio utilities ### Platform-Specific macOS window blur effects macOS font enumeration ## Development Tools ### Linting & Formatting Fast linter + formatter (replaces ESLint + Prettier) * Auto-organizes imports * Enforces code style * TypeScript-first ### Testing Vite-native test runner * Unit tests for utilities * Integration tests for editor extensions ### Type Definitions React type definitions React DOM type definitions ## Package Manager Fast, disk-efficient package manager Only builds native modules when needed: ```json package.json theme={null} "pnpm": { "onlyBuiltDependencies": ["@biomejs/biome", "esbuild"] } ``` ## Icon Library Open-source React icon library Core icon set ## Version Matrix | Component | Version | Notes | | --------- | ------------- | ----------------- | | Node.js | 18+ | Required for pnpm | | Rust | 1.70+ | 2021 edition | | macOS | 11+ | Big Sur or later | | Windows | 10+ | 64-bit | | Linux | Ubuntu 20.04+ | Debian-based | ## Architecture Decisions ### Why Tauri over Electron? * Smaller bundle size (\~10MB vs 100MB+) * Lower memory footprint * Native system integration * Rust security guarantees ### Why TipTap over other editors? * TypeScript-first * Full control over markdown serialization * Extensible plugin system * ProseMirror foundation (battle-tested) ### Why Biome over ESLint/Prettier? * 10-100x faster * Single tool for linting + formatting * Built in Rust * Auto-import organization ### Why Rig over LangChain? * Rust-native (type-safe) * Multi-provider abstraction * Streaming support * Tool calling with JSON schema # Testing Guide Source: https://docs.glyphformac.com/development/testing Running tests and writing new test cases ## Test Framework Glyph uses **Vitest** for frontend testing: * Fast execution (powered by Vite) * Jest-compatible API * Native TypeScript support * Watch mode with HMR Rust backend tests use Rust's built-in `cargo test` framework. ## Running Tests ### All Tests ```bash theme={null} pnpm test # Runs all tests once and exits ``` ### Watch Mode ```bash theme={null} pnpm test:watch # Re-runs tests on file changes # Shows interactive UI ``` ### Single Test File ```bash theme={null} pnpm test -- src/lib/diff.test.ts # Only runs tests in diff.test.ts ``` ### Single Test Case ```bash theme={null} pnpm test -- -t "computes line diff" # Runs tests matching the name ``` ### Coverage Report ```bash theme={null} pnpm test -- --coverage # Generates coverage report in coverage/ ``` ## Frontend Test Structure ### Test File Naming * Place tests next to source: `utils.ts` → `utils.test.ts` * Use `.test.ts` or `.test.tsx` extension * Integration tests: `.integration.test.ts` ### Example Test ```typescript src/lib/diff.test.ts theme={null} import { describe, it, expect } from 'vitest'; import { computeLineDiff } from './diff'; describe('computeLineDiff', () => { it('computes line diff for simple change', () => { const oldText = 'Hello\nWorld'; const newText = 'Hello\nGlyph'; const diff = computeLineDiff(oldText, newText); expect(diff).toEqual([ { type: 'unchanged', value: 'Hello' }, { type: 'removed', value: 'World' }, { type: 'added', value: 'Glyph' } ]); }); it('handles empty strings', () => { expect(computeLineDiff('', '')).toEqual([]); }); }); ``` ## Testing Patterns ### Utility Functions ```typescript Pure Function Test theme={null} import { parentDir } from './path'; it('extracts parent directory', () => { expect(parentDir('notes/daily/2024-03-15.md')).toBe('notes/daily'); expect(parentDir('notes/example.md')).toBe('notes'); expect(parentDir('example.md')).toBe(''); }); ``` ```typescript Edge Cases theme={null} it('handles edge cases', () => { expect(parentDir('')).toBe(''); expect(parentDir('/')).toBe(''); expect(parentDir('no-slash')).toBe(''); }); ``` ### React Hooks ```typescript src/hooks/useFileTree.test.ts theme={null} import { renderHook, waitFor } from '@testing-library/react'; import { useFileTree } from './useFileTree'; it('loads directory entries', async () => { const { result } = renderHook(() => useFileTree({ spacePath: '/test/space', // ... other deps })); await result.current.loadDir('notes'); await waitFor(() => { expect(result.current.entries).toHaveLength(3); }); }); ``` ### TipTap Extensions ```typescript src/components/editor/extensions/wikiLink.integration.test.ts theme={null} import { describe, it, expect } from 'vitest'; import { createEditor } from '@tiptap/core'; import { WikiLink } from './wikiLink'; describe('WikiLink extension', () => { it('parses [[wiki links]]', () => { const editor = createEditor({ extensions: [WikiLink], content: 'See [[example-note]] for details' }); const json = editor.getJSON(); expect(json.content[0].content[1].type).toBe('wikiLink'); expect(json.content[0].content[1].attrs.target).toBe('example-note'); }); }); ``` ### Mocking Tauri Commands ```typescript src/lib/tauri.mock.ts theme={null} import { vi } from 'vitest'; // Mock the invoke function export const mockInvoke = vi.fn(); vi.mock('@tauri-apps/api/core', () => ({ invoke: mockInvoke })); ``` ```typescript Usage in test theme={null} import { mockInvoke } from './tauri.mock'; import { invoke } from '@/lib/tauri'; it('calls space_open command', async () => { mockInvoke.mockResolvedValueOnce({ root: '/path/to/space', schema_version: 1 }); const result = await invoke('space_open', { path: '/path/to/space' }); expect(mockInvoke).toHaveBeenCalledWith('space_open', { path: '/path/to/space' }); expect(result.root).toBe('/path/to/space'); }); ``` ## Rust Testing ### Unit Tests ```rust src-tauri/src/paths.rs theme={null} #[cfg(test)] mod tests { use super::*; #[test] fn test_join_under_safe_path() { let base = PathBuf::from("/space"); let result = join_under(&base, "notes/example.md"); assert_eq!(result.unwrap(), PathBuf::from("/space/notes/example.md")); } #[test] fn test_join_under_rejects_traversal() { let base = PathBuf::from("/space"); let result = join_under(&base, "../../../etc/passwd"); assert!(result.is_err()); } } ``` Run with: ```bash theme={null} cd src-tauri cargo test ``` ### Integration Tests ```rust src-tauri/tests/space_lifecycle.rs theme={null} use glyph_lib::space; #[test] fn test_create_and_open_space() { let temp_dir = tempdir().unwrap(); let space_path = temp_dir.path().to_str().unwrap(); // Create space let info = space::space_create(space_path.to_string()).unwrap(); assert_eq!(info.schema_version, 1); // Verify structure assert!(temp_dir.path().join("notes").exists()); assert!(temp_dir.path().join("assets").exists()); assert!(temp_dir.path().join("space.json").exists()); } ``` ## Test Coverage ### Current Coverage Run coverage report: ```bash theme={null} pnpm test -- --coverage ``` Output: ``` ---------------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ---------------------|---------|----------|---------|---------|------------------- All files | 67.82 | 58.33 | 71.43 | 67.82 | src/lib/diff.ts | 100 | 100 | 100 | 100 | src/lib/path.ts | 85.71 | 66.67 | 100 | 85.71 | 12-15 src/utils/ | 45.23 | 33.33 | 50.00 | 45.23 | ---------------------|---------|----------|---------|---------|------------------- ``` ### Coverage Goals * **Utilities**: 90%+ coverage (pure functions) * **Hooks**: 70%+ coverage (harder to test) * **Components**: 50%+ coverage (UI-heavy) * **Integration**: Key workflows covered ## Test Organization ### Tested Modules * `src/lib/diff.test.ts` - Text diffing * `src/lib/shortcuts.test.ts` - Keyboard shortcuts * `src/lib/notePreview.test.ts` - Preview generation * `src/lib/errorUtils.test.ts` - Error handling * `src/utils/path.test.ts` - Path utilities * `src/components/editor/extensions/wikiLink.integration.test.ts` * `src/components/editor/extensions/markdownImage.integration.test.ts` * `src/components/editor/extensions/table.integration.test.ts` * `src/components/editor/markdown/wikiLinkCodec.test.ts` * `src/lib/database/config.test.ts` - Config validation * `src/lib/database/board.test.ts` - Board layout * `src/hooks/database/useDatabaseTable.test.ts` * `src/hooks/fileTreeHelpers.test.ts` * `src/lib/canvasLayout.test.ts` ## Writing New Tests ### Step 1: Create Test File ```bash theme={null} # Create next to source file touch src/lib/myfeature.test.ts ``` ### Step 2: Import Vitest ```typescript theme={null} import { describe, it, expect, beforeEach, afterEach } from 'vitest'; ``` ### Step 3: Group Tests ```typescript theme={null} describe('MyFeature', () => { describe('basic functionality', () => { it('does something', () => { // Test code }); }); describe('edge cases', () => { it('handles empty input', () => { // Test code }); }); }); ``` ### Step 4: Write Assertions ```typescript Equality theme={null} expect(value).toBe(42); expect(object).toEqual({ key: 'value' }); expect(array).toHaveLength(3); ``` ```typescript Truthiness theme={null} expect(value).toBeTruthy(); expect(value).toBeFalsy(); expect(value).toBeNull(); expect(value).toBeUndefined(); ``` ```typescript Strings theme={null} expect(text).toContain('substring'); expect(text).toMatch(/regex/); ``` ```typescript Arrays theme={null} expect(array).toContain(item); expect(array).toContainEqual({ key: 'value' }); ``` ```typescript Exceptions theme={null} expect(() => dangerousFunction()).toThrow(); expect(() => dangerousFunction()).toThrow('Error message'); ``` ## Continuous Integration Tests run on every PR via GitHub Actions: ```yaml .github/workflows/test.yml theme={null} name: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 with: version: 10.28.2 - uses: actions/setup-node@v3 with: node-version: 18 cache: pnpm - run: pnpm install - run: pnpm test - run: cd src-tauri && cargo test ``` ## Best Practices Focus on what the function does, not how it does it. ```typescript Good theme={null} it('filters markdown files', () => { const files = ['a.md', 'b.txt', 'c.md']; expect(filterMarkdown(files)).toEqual(['a.md', 'c.md']); }); ``` ```typescript Bad theme={null} it('uses Array.filter internally', () => { const spy = vi.spyOn(Array.prototype, 'filter'); filterMarkdown(['a.md']); expect(spy).toHaveBeenCalled(); // Too implementation-specific }); ``` Each test should verify one thing. ```typescript Good theme={null} it('parses frontmatter title', () => { expect(parseFrontmatter('---\ntitle: Hello\n---').title).toBe('Hello'); }); it('parses frontmatter tags', () => { expect(parseFrontmatter('---\ntags: [a, b]\n---').tags).toEqual(['a', 'b']); }); ``` Test name should explain what's being tested. ```typescript Good theme={null} it('rejects path traversal with ../', () => { ... }); ``` ```typescript Bad theme={null} it('works', () => { ... }); ``` Always test boundary conditions. ```typescript theme={null} describe('splitLines', () => { it('handles empty string', () => { expect(splitLines('')).toEqual([]); }); it('handles single line', () => { expect(splitLines('hello')).toEqual(['hello']); }); it('handles multiple lines', () => { expect(splitLines('a\nb\nc')).toEqual(['a', 'b', 'c']); }); }); ``` ## Next Steps Understand the codebase structure Learn about React components # AI Chat Assistant Source: https://docs.glyphformac.com/features/ai-chat Multi-provider AI chat with context attachment and conversation history Glyph includes a built-in AI assistant that supports multiple providers (OpenAI, Anthropic, Google, Ollama, and more). Attach notes and folders as context to ground conversations in your knowledge base. ## Overview The AI panel is a resizable sidebar that provides: * **Multi-provider support**: OpenAI, Anthropic, Gemini, Ollama, OpenRouter, and custom endpoints * **Context attachment**: Include notes and folders in your prompts * **Conversation history**: Review and restore past chats * **Two modes**: Chat (conversational) and Create (single-shot generation) * **Profile management**: Switch between API keys and models instantly AI chat panel with context attachments ## Getting Started Go to Settings → AI to configure your first profile. Choose a provider, add your API key, and select a model. Click the AI icon in the sidebar or use the keyboard shortcut. Type your message and press Enter to send. ## AI Providers Glyph supports multiple AI providers out of the box: GPT-4, GPT-4 Turbo, GPT-3.5 Turbo, and o-series reasoning models with configurable reasoning effort. Claude 3.5 Sonnet, Opus, and Haiku models with extended context windows. Gemini 1.5 Pro, Flash, and other Google AI models. Run local models like Llama, Mistral, and Phi directly on your machine. Access dozens of models through a single API with unified pricing. Connect to any OpenAI-compatible endpoint with custom base URLs and headers. ### Provider Configuration Each provider requires: * **Name**: Display name for the profile (e.g., "Work GPT-4") * **Provider**: Select from the dropdown * **Model**: Choose an available model * **API Key**: Your provider's API key (stored securely) * **Base URL** (optional): Custom endpoint for compatible providers * **Headers** (optional): Additional HTTP headers for authentication API keys are stored locally using your system's secure credential storage. They never leave your machine. ## Chat Modes ### Chat Mode **Conversational assistant** with full context awareness: * Multi-turn conversations with memory * Follow-up questions and clarifications * Context persists across messages * Ideal for research, brainstorming, and exploration ### Create Mode **Single-shot generation** for focused tasks: * Optimized for content creation * Each request is independent * Less conversational, more generative * Ideal for drafting, summarizing, and transforming content Switch modes in Settings → AI → Assistant Mode. ## Context Attachments Attach notes and folders to ground AI responses in your knowledge: ### Adding Context Type `@` in the composer to search for files and folders. Select one to attach it. Click the add context button to browse and select files/folders. Drag notes from the file tree into the AI panel to attach them. ### Context Types Attach a single note. Its full markdown content is included in the prompt. Attach an entire folder. All markdown files within are concatenated and sent. ### Context Budget Glyph automatically manages context size: * **Character budget**: 100,000 characters by default * **Token estimation**: Displays estimated token count before sending * **Truncation**: If content exceeds budget, files are truncated in order * **Manifest**: View exactly what's included in the "Attached" section ### Removing Context Click the X on any attached item to remove it. Context persists for the current conversation until manually cleared or you start a new chat. ## Using the AI Panel ### Panel Layout 1. **Header**: Model selector, new chat button, settings, minimize 2. **History**: Expandable list of recent conversations 3. **Chat thread**: Messages from you and the AI 4. **Composer**: Input area with context attachments and send button ### Model Selector Click the model dropdown to: * Switch models without losing your conversation * View model details (context length, pricing, modalities) * Filter models by name or ID * Switch between API key profiles Model selector showing available models with details ### Sending Messages Use @ mentions or the + button to add relevant notes/folders. Enter your prompt in the composer. Shift+Enter for new lines. Press Enter or click the send button to submit. Streaming response appears in the chat thread. ### Message Actions Click the copy icon to copy AI responses to your clipboard. Click the save icon to append the response to a note of your choice. Click retry to regenerate the last response (uses same context). Click stop during streaming to halt the response early. ## Conversation History ### Viewing History Click the history toggle to expand the history panel: * Shows last 14 conversations * Each entry displays title, model, provider, and timestamp * Click any conversation to restore it ### Restoring Conversations Click the history button to expand the panel. Click any conversation to load its full message history. Add new messages to continue where you left off. ### Conversation Metadata Each conversation stores: * **Job ID**: Unique identifier * **Title**: Auto-generated from first message * **Provider**: Which AI service was used * **Model**: Specific model name * **Created**: Timestamp * **Message count**: Number of messages exchanged * **Preview**: First line of the conversation ### Starting Fresh Click the "New Chat" button to: * Clear the current conversation * Reset attached context * Start with a blank slate New chat does not save the current conversation. Make sure to finish your current chat before starting a new one. ## Profile Management ### Creating Profiles Navigate to Settings → AI. Create a new profile with a descriptive name. Select your provider and enter the required details. Enter your API key (stored securely on your system). Choose from the provider's available models. ### Switching Profiles If you have multiple profiles configured: * Click the model selector * Use the "API keys" pill switcher at the top * Click a profile to switch instantly * Model list updates to show that provider's models ### Use Cases for Multiple Profiles * **Work/Personal**: Separate API keys for different contexts * **Cost optimization**: Use cheaper models for drafts, expensive ones for final output * **Local/Cloud**: Switch between Ollama (free) and OpenAI (paid) * **Provider comparison**: Test same prompt across different providers ## Advanced Features ### Reasoning Effort (o-series models) For OpenAI's o-series reasoning models: * Configure reasoning effort: low, medium, high * Higher effort = more thinking time = better answers (and higher cost) * Set in profile configuration ### Custom Headers Add custom HTTP headers for authentication or routing: ```json theme={null} [ {"key": "X-API-Version", "value": "2024-01-01"}, {"key": "X-Organization", "value": "your-org"} ] ``` ### Private Hosts Enable "Allow private hosts" to connect to local AI services: * Required for Ollama ([http://localhost:11434](http://localhost:11434)) * Required for self-hosted endpoints on private IPs * Security warning: Only enable for trusted endpoints ### Tool Timeline (Create Mode) When using Create mode, the AI may use tools: * **create\_note**: Creates a new note in your space * **search\_notes**: Searches your knowledge base * **list\_files**: Lists files in a directory The tool timeline shows: * Which tools were called * Tool parameters * Results or errors * Execution timing AI tool timeline showing create_note and search_notes calls ## Context Strategies ### Minimal Context **When**: Quick questions, general knowledge **Approach**: Send prompts without attachments ``` Explain the concept of atomic notes ``` ### Single Note Context **When**: Analyzing, summarizing, or transforming a specific note **Approach**: Attach the target note ``` @daily/2024-03-10.md Summarize the key points from this daily note ``` ### Folder Context **When**: Cross-referencing multiple notes, finding patterns **Approach**: Attach a relevant folder ``` @projects/client-work What are the common themes across these project notes? ``` ### Mixed Context **When**: Comparing specific notes against broader context **Approach**: Attach multiple files and folders ``` @projects/q1-goals.md @projects/completed How do my Q1 goals compare to what I actually completed? ``` ## Best Practices 1. **Be specific with prompts** - Clear instructions yield better results 2. **Use context strategically** - More context ≠ better answers; be selective 3. **Review token estimates** - Stay within model limits for best performance 4. **Leverage history** - Continue conversations instead of repeating context 5. **Try different models** - Each excels at different tasks 6. **Use Create mode for generation** - Chat mode for back-and-forth 7. **Save useful responses** - Click save to preserve insights in your notes 8. **Name profiles clearly** - "Work GPT-4" vs "Personal Claude" helps when switching ## Keyboard Shortcuts | Shortcut | Action | | ----------- | ---------------------- | | Enter | Send message | | Shift+Enter | New line in composer | | Escape | Minimize AI panel | | @ | Trigger context picker | | Click model | Open model selector | ## Privacy & Security * **API keys**: Stored in system credential manager (Keychain on macOS, Credential Manager on Windows) * **Conversations**: Stored locally in your space's `.glyph` directory * **Context**: Only sent when explicitly attached * **No telemetry**: Glyph doesn't log or transmit your prompts * **Provider privacy**: Subject to each AI provider's privacy policy AI providers may retain conversation logs according to their policies. Do not send sensitive information unless you trust the provider. ## Common Issues ### "No API key configured" **Solution**: Add an API key for the selected profile in Settings → AI ### "Model not available" **Solution**: Check that your API key has access to the model, or select a different model ### "Context too large" **Solution**: Remove some attachments or use a model with a larger context window ### Streaming stops early **Possible causes**: * Network interruption * Provider rate limiting * Model hit token limit **Solution**: Click retry or reduce context size ## Related Features * [Tags & Search](/features/tags-search) - Find notes to attach as context * [Daily Notes](/features/daily-notes) - Use AI to reflect on daily notes * [Attachments](/features/attachments) - Include images in AI conversations (vision models) * [AI Setup](/ai/setup) - Configure AI providers and profiles # File Attachments Source: https://docs.glyphformac.com/features/attachments Attach images and files to notes with content-addressed storage Glyph allows you to attach files to your notes with a content-addressed storage system. Files are deduplicated using SHA256 hashing and stored in your space's `assets/` directory. ## Overview Attachments in Glyph: * **Content-addressed**: Files are named by their SHA256 hash * **Deduplicated**: Identical files are stored only once * **Persistent**: Files remain available even if the source is deleted * **Portable**: All assets travel with your space * **Efficient**: No file size bloat from duplicates Note with image attachment ## Attaching Files ### From the Editor Navigate to the note where you want to add an attachment. Click the attachment button in the editor toolbar or use the slash command `/attach`. Choose a file from your system using the file picker. Glyph copies the file to `assets/`, generates a hash-based filename, and inserts markdown. ### Supported File Types Glyph handles all file types but provides special treatment for: **Images** (auto-embedded): * PNG (`.png`) * JPEG (`.jpg`, `.jpeg`) * GIF (`.gif`) * WebP (`.webp`) * SVG (`.svg`) **Other files** (linked): * PDFs, documents, spreadsheets, etc. * Generic files use standard markdown links ## Content-Addressed Storage ### How It Works You choose a file to attach (e.g., `screenshot.png`). Glyph computes the file's SHA256 hash while copying it. File is saved as `.` in the `assets/` directory. A relative link to the asset is inserted in your note. ### Example **Original file**: `~/Downloads/diagram.png` **SHA256 hash**: `a3b5c8d9e1f2...` (64 hex characters) **Stored as**: `.glyph/assets/a3b5c8d9e1f2...png` **Markdown**: `![](../assets/a3b5c8d9e1f2...png)` ### Benefits Attach the same image to 10 notes, it's stored only once. File corruption is detectable—hash mismatch means the file changed. Copy your space folder anywhere, all assets come with it. Hash-based names eliminate filename collisions. ## Markdown Syntax ### Images Images use standard markdown image syntax: ```markdown theme={null} ![](../assets/a3b5c8d9e1f2...png) ``` Glyph renders these inline in the editor with preview support. ### Other Files Non-image files use standard link syntax: ```markdown theme={null} [document.pdf](../assets/b4c6d8e0f3a1...pdf) ``` Clicking the link opens the file in your system's default application. ### Relative Paths Asset links use **relative paths**: * Notes in root: `../assets/` * Notes in subfolders: `../../assets/` or deeper depending on note location Glyph automatically computes the correct relative path when inserting attachments. ## Storage Location All assets live in: ``` your-space/ .glyph/ assets/ a3b5c8d9e1f2...png b4c6d8e0f3a1...pdf c5d7e9f1a2b3...jpg ``` ### Why `.glyph/assets/`? * **Centralized**: All assets in one place * **Hidden**: `.glyph` is a hidden directory (won't clutter your file browser) * **Protected**: Separate from your notes, less likely to be accidentally deleted * **Portable**: Move `.glyph` with your notes, everything stays linked ## File Preview Glyph provides in-app preview for attachments: ### Image Preview Images display inline in the editor: * Click to expand full-size * Drag to reposition (markdown syntax) * Alt text supported for accessibility ### File Info Hover over a file link to see: * Filename (original extension preserved) * File size * MIME type ### External Opening Click non-image file links to open in your system's default app: * PDFs → PDF viewer * Documents → Word/LibreOffice/etc. * Spreadsheets → Excel/Sheets/etc. ## Managing Attachments ### Finding Unused Assets Glyph doesn't automatically delete assets when you remove markdown links. To clean up: Search `.glyph/assets/` for files not referenced in any note. Write a script to compare asset filenames against all markdown files. Remove unused files from the `assets/` directory. Future versions may include automatic orphan detection and cleanup. ### Renaming Assets Don't rename files in `assets/` manually: * Hash-based names are **computed**, not arbitrary * Renaming breaks all markdown references * Re-attach the file instead to generate a new hash ### Moving Notes When moving notes between folders: * **Glyph auto-updates** relative asset paths * Links remain functional after the move * No need to manually fix `../assets/` paths ### Backing Up Assets Include `.glyph/assets/` in your backup strategy: * Git: Add `.glyph/assets/` to your repository * Cloud sync: Ensure `.glyph` is not ignored * Manual backups: Copy entire space folder including `.glyph` ## File Size Considerations ### Storage Limits Glyph has no built-in file size limits, but consider: * **Large files slow sync**: If using Git or cloud sync, big assets increase sync time * **Disk space**: Assets accumulate over time * **Performance**: Massive image files may slow rendering ### Recommendations * **Optimize images**: Use compressed formats (WebP, JPEG) over raw (PNG) * **Limit file sizes**: Keep individual files under 10 MB for best performance * **Use external hosting for large media**: Link to YouTube, Vimeo, etc. instead of embedding large videos ## Advanced: Manual Attachment API Attach files programmatically using the Tauri command: ```typescript theme={null} import { invoke } from '@tauri-apps/api/core'; const result = await invoke('note_attach_file', { note_id: 'daily/2024-03-10', source_path: '/Users/you/Downloads/diagram.png' }); // result.asset_rel_path: "assets/a3b5c8d9e1f2...png" // result.markdown: "![](../assets/a3b5c8d9e1f2...png)" ``` **Returns**: * `asset_rel_path`: Path relative to space root * `markdown`: Ready-to-insert markdown syntax ### Use Cases * Custom importers * Automation scripts * Plugin development * Batch attachment processing ## Atomic Writes File imports use **atomic writes** for safety: File is copied to `.glyph/assets/.import.tmp.`. SHA256 is computed during the copy. Temp file is renamed to hash-based name in one operation. **Benefits**: * **Crash-safe**: Interrupted imports don't corrupt assets * **Deduplication-aware**: If hash already exists, temp file is discarded * **No partial writes**: File appears only when fully written ## Deduplication Example ### Scenario You have the same screenshot in two places: * `~/Desktop/bug-report.png` * `~/Downloads/bug-report-copy.png` Both are identical files (same bytes). ### Attachment Process Attach `~/Desktop/bug-report.png` to `notes/project-a.md`. → Stored as `assets/a3b5c8d9e1f2...png` Attach `~/Downloads/bug-report-copy.png` to `notes/project-b.md`. → Same hash, file already exists, no new copy made ### Result Both notes reference the same asset: **notes/project-a.md**: ```markdown theme={null} ![](../assets/a3b5c8d9e1f2...png) ``` **notes/project-b.md**: ```markdown theme={null} ![](../assets/a3b5c8d9e1f2...png) ``` Only **one file** stored in `assets/`, saving disk space. ## Security ### SHA256 Hashing * **Collision-resistant**: Virtually impossible to create two files with the same hash * **Integrity checking**: Verify file hasn't been tampered with * **Not encryption**: Files are stored unencrypted (plaintext) ### SSRF Protection Glyph's attachment system is local-only: * No URL-based attachments (prevents SSRF attacks) * All files come from your filesystem * No remote fetching or automatic downloads ### Permissions Attachments require: * **Read permission** on source file * **Write permission** on `.glyph/assets/` directory Glyph respects your OS's file permissions. ## Keyboard Shortcuts | Shortcut | Action | | ----------- | ------------------- | | `/attach` | Open file picker | | Click image | Expand preview | | Click link | Open in default app | ## Best Practices 1. **Optimize before attaching** - Compress images to reduce space 2. **Use descriptive alt text** - Helps with accessibility and searchability 3. **Don't rename in assets/** - Always re-attach instead 4. **Include assets in backups** - Don't ignore `.glyph/assets/` 5. **Clean up orphans periodically** - Prevent asset bloat over time 6. **Use external hosting for videos** - Embed YouTube/Vimeo instead of attaching large files 7. **Test links after moving notes** - Verify relative paths still work ## Limitations * No built-in orphan detection (yet) * No automatic thumbnail generation * No image editing/cropping in-app * No direct camera/screenshot integration (use system tools, then attach) * No cloud asset hosting (files are local only) ## Related Features * [AI Chat](/features/ai-chat) - Attach images to AI conversations with vision models * [Tags & Search](/features/tags-search) - Find notes containing specific images * [Markdown Editor](/features/markdown-editor) - Embed images in your notes * [Space System](/development/space-system) - Learn about content-addressed storage # Daily Notes Source: https://docs.glyphformac.com/features/daily-notes Capture thoughts and logs with automatically dated notes Daily notes provide a frictionless way to capture thoughts, journal entries, or daily logs. Glyph automatically creates a note for today's date with a single click. ## Setting Up Daily Notes Configure daily notes in the Settings: Click the settings icon or press `Cmd+,` (macOS) / `Ctrl+,` (Windows/Linux). Select the **Daily Notes** tab in the sidebar. Choose where daily notes should be created (e.g., `journal/`, `daily/`, or leave blank for root). The folder path must be relative to your space root. It will be created automatically if it doesn't exist. ## Creating Daily Notes Once configured, create today's note: Click the **Daily Note** button in the main toolbar. Press the daily note keyboard shortcut (customizable in settings). Open the command palette and search for "Open Daily Note". Glyph will: 1. Check if today's note already exists 2. If it exists, open it 3. If not, create a new note with today's date as the filename 4. Open the note in the editor ## File Naming Daily notes use ISO date format for filenames: ``` YYYY-MM-DD.md ``` **Examples:** * `2024-01-15.md` * `2024-12-31.md` * `2026-03-03.md` This format ensures: * **Chronological sorting**: Files sort naturally by date * **International standard**: ISO 8601 date format * **No ambiguity**: Year, month, and day are always clear ## Default Content When Glyph creates a new daily note, it starts with: ```markdown theme={null} # YYYY-MM-DD ``` The heading matches the filename, giving you a clean starting point to add content. ## Folder Organization Choose a folder structure that fits your workflow: **Folder: `daily/`** All daily notes in one folder: ``` daily/ 2024-01-01.md 2024-01-02.md 2024-01-03.md ... ``` Simple and easy to browse. Works well if you search more than you browse. **Folder: `journal/`** Manually organize by year: ``` journal/ 2024/ 2024-01-01.md 2024-12-31.md 2025/ 2025-01-01.md ``` Better for long-term journaling. You'll need to create year folders manually. **Folder: (empty)** Daily notes in your space root: ``` 2024-01-01.md 2024-01-02.md Projects/ Reference/ ``` Quick access, but can clutter your root folder over time. ## Workflow Tips ### Morning Routine 1. Open Glyph 2. Click the daily note button 3. Start writing your thoughts, tasks, or goals for the day ### Task Tracking Combine daily notes with task lists: ```markdown theme={null} # 2024-01-15 ## Tasks - [ ] Review project proposal - [ ] Call client - [ ] Update documentation ## Notes - Met with design team... ``` Tasks from daily notes appear in the Tasks pane when scheduled. ### Linking Daily Notes Reference other daily notes using wikilinks: ```markdown theme={null} # 2024-01-15 Follow-up from [[2024-01-14]] Planning for [[2024-01-16]] ``` ### Templates (Manual) Create a template note and copy its content to new daily notes: ```markdown theme={null} # YYYY-MM-DD ## Morning Review ## Tasks ## Notes ## Evening Reflection ``` Keep a `_daily-template.md` file in your daily notes folder to copy from when you want a structured format. ## Integration with Other Features ### Search Daily notes are indexed like any other note: * Search content: Find specific entries across all dates * Filter by tag: Use tags in daily notes and search by them * Recent notes: Daily notes appear in "Recent" views ### Tags Add tags to categorize daily entries: ```markdown theme={null} # 2024-01-15 #journal #reflection Today I learned... ``` ### Backlinks When you link to other notes from your daily notes, those notes show the backlink: ```markdown theme={null} # 2024-01-15 Discussed [[Project Alpha]] with the team. ``` The "Project Alpha" note will show a backlink from your daily note. ## Advanced: Custom Date Formats Glyph currently only supports ISO date format (YYYY-MM-DD) for daily notes. Custom formats are not available. The ISO format is intentionally the only option because: * **Sortability**: Files sort chronologically in any file browser * **Consistency**: No ambiguity between date formats * **Compatibility**: Works across all operating systems and locales ## Troubleshooting ### Daily note button doesn't work **Solution**: Check that you've set a daily notes folder in Settings > Daily Notes. ### Note created in wrong location **Solution**: Verify your daily notes folder path is relative (e.g., `journal/`, not `/journal/`). ### Can't find old daily notes **Solution**: Use search to find notes by date or content. Search for `2024-01` to find all January 2024 notes. # Database Views Source: https://docs.glyphformac.com/features/databases Organize notes as structured databases with flexible table and board layouts Database views transform folders, tags, or search results into structured tables and boards. Each database is a special markdown file that displays your notes as rows with customizable columns and properties. ## Creating a Database Databases are markdown files with a special structure: 1. Create a new note 2. Configure the source (folder, tag, or search query) 3. Add columns for the properties you want to track 4. Switch between table and board views Database configurations are stored in the note's frontmatter, making them version-controllable and shareable. ## Data Sources Databases can pull notes from three types of sources: Display all notes from a specific folder, with optional recursive subfolder inclusion. Aggregate all notes that use a specific tag across your entire space. Use advanced search queries to dynamically filter notes based on content and metadata. ### Folder Sources When using folder sources: * **Folder path**: Choose any folder in your space * **Recursive**: Toggle to include/exclude subfolders * **Scope**: Non-recursive shows only direct children ### Tag Sources Tag sources aggregate notes across your entire space: * All notes with the specified tag appear as rows * Updates automatically when you tag/untag notes * Great for project management and cross-cutting concerns ### Search Sources Use Glyph's advanced search syntax: ``` tag:projects "roadmap" ``` Search sources update as your content changes, providing dynamic views into your knowledge base. ## Table View The table layout displays notes as rows with configurable columns: Database table view showing notes with multiple columns ### Built-in Columns Every database has access to these system columns: * **Title**: Note title extracted from frontmatter or first heading * **Tags**: All tags associated with the note * **Path**: Relative path within your space * **Created**: Note creation timestamp * **Updated**: Last modification timestamp ### Custom Property Columns Add columns for any frontmatter property: Click the columns button in the database toolbar to manage your columns. Choose from existing properties or create new ones. Glyph automatically detects properties from your notes. Set the label, icon, width, and visibility for each column. ### Property Types Glyph supports multiple property types: * **Text**: Single-line text values * **Boolean**: True/false checkboxes * **Tags**: Multi-select tag values * **Date**: ISO date strings (YYYY-MM-DD) ### Sorting Click any column header to sort: * **First click**: Sort ascending * **Second click**: Sort descending * **Third click**: Clear sort Only one sort is active at a time. ### Inline Editing Click any cell to edit it directly: * Text fields open inline editors * Boolean fields toggle with a single click * Tags open a multi-select picker * Changes save automatically to the note's frontmatter ## Board View Board view organizes notes into vertical lanes using a grouping column: Database board view with cards organized in lanes ### Setting Up a Board Boards require a single-value property column (text, boolean, or single tag) to group cards. Click the board icon in the toolbar to switch layouts. Use the dropdown in the toolbar to choose which column groups the cards. ### Board Lanes Each unique value in the grouping column becomes a lane: * **Status property**: "Todo", "In Progress", "Done" lanes * **Boolean property**: "True" and "False" lanes * **Tag property**: One lane per unique tag value ### Card Layout Each card displays: 1. **Title**: Note title (or filename fallback) 2. **Preview**: First 100 characters of note content 3. **Tags**: Up to 4 tags (excluding the grouping tag) 4. **Additional column**: First visible non-title column 5. **Path**: Note location for reference ### Drag and Drop Move cards between lanes by dragging: * Dragging updates the grouping property automatically * Right-click for a context menu with "Move to" options * Double-click a card to open the full note ## Filters Narrow your database view with column filters: ### Filter Operators * **Contains**: Text match (case-insensitive) * **Equals**: Exact match * **Is empty**: No value set * **Is not empty**: Any value present * **Is true**: Boolean = true * **Is false**: Boolean = false ### Multiple Filters Add multiple filters to create precise views: * All filters use AND logic (all must match) * Filters apply to both table and board views * Filter count shows in the toolbar ```yaml Example: Status filter theme={null} --- filters: - column_id: "property:status" operator: "equals" value_text: "active" --- ``` ```yaml Example: Tag filter theme={null} --- filters: - column_id: "tags" operator: "tags_contains" value_list: ["project"] --- ``` ## Creating New Rows Add notes directly from the database: The toolbar has an "Add row" button to create new notes. New notes appear in the configured "New Rows" folder with the specified title prefix. If the new note matches your filters and source, it appears as a row instantly. ### Configuring New Rows In the source dialog, configure: * **Target folder**: Where new notes are created * **Title prefix**: Default prefix for new note titles (e.g., "Project -") ## Column Management Customize which columns appear and how they're displayed: ### Column Properties * **Label**: Display name in the header * **Icon**: Visual indicator (from a curated set) * **Width**: Column width in pixels (default 180px) * **Visible**: Toggle visibility without deleting ### Reordering Columns Drag column rows in the columns dialog to reorder them in the table. ### Column Icons Choose from semantic icons: * Text properties: Document, note, text icons * Boolean properties: Checkbox, toggle icons * Tag properties: Tag, folder, category icons * Date properties: Calendar, clock icons ## Real-time Updates Databases stay synchronized with your notes: * **File changes**: Database reloads when source notes change * **Cell edits**: Updates write to frontmatter immediately * **New notes**: Automatically appear if they match the source * **Deleted notes**: Removed from database view instantly Recent local mutations are debounced to prevent reload flicker during rapid edits. ## Advanced Configuration Database configurations are stored in the note's frontmatter: ```yaml Complete example theme={null} --- database: version: 1 source: kind: folder value: projects recursive: true new_note: folder: projects title_prefix: "Project" view: layout: board board_group_by: "property:status" columns: - id: title type: title label: Title width: 300 visible: true - id: "property:status" type: property label: Status property_key: status property_kind: text icon: checkbox width: 180 visible: true sorts: - column_id: updated direction: desc filters: - column_id: "property:status" operator: is_not_empty value_list: [] --- ``` ## Keyboard Shortcuts | Shortcut | Action | | ---------------- | ------------------ | | Click header | Toggle sort | | Click cell | Edit value | | Double-click row | Open note | | Drag card | Move between lanes | | Right-click | Context menu | ## Use Cases Track project status, priority, and deadlines with a board view grouped by status. Maintain a table of articles with rating, tags, and read status. Sort meetings by date with attendees, topics, and action items columns. Aggregate papers by topic tag with custom properties for authors, year, and citations. ## Best Practices 1. **Start with folder sources** for simplicity, then graduate to tags or search 2. **Use board view for workflows** (todo → doing → done) 3. **Keep column count low** (5-7 visible columns) for readability 4. **Leverage filters** instead of creating multiple similar databases 5. **Name grouping properties semantically** ("status" not "column1") 6. **Set reasonable column widths** to avoid horizontal scrolling ## Limitations * Only one active sort at a time * Board view requires a single-value grouping column * Maximum 2000 rows loaded (configurable limit) * No nested grouping in board view * Filters use AND logic only (no OR) ## Related Features * [Task Management](/features/task-management) - Track todos within notes * [Tags & Search](/features/tags-search) - Advanced search syntax for database sources * [Notes](/features/notes) - Understanding note properties and frontmatter # File Tree Source: https://docs.glyphformac.com/features/file-tree Navigate and organize your notes with the file tree sidebar The file tree provides a hierarchical view of all files and folders in your Glyph space, making it easy to navigate and organize your notes. ## File Tree Overview The file tree appears on the left side of the Glyph window and displays: * **Folders**: Expandable/collapsible directory structure * **Files**: All files in your space (markdown and non-markdown) * **Active file**: Highlighted in the tree * **Folder depth**: Visual indentation and connecting lines ## Navigation ### Opening Files Click any file in the tree to open it in the main editor pane. The currently open file is highlighted with a distinct background. Some file types open in preview mode, showing a read-only rendering. ### Expanding Folders * **Click folder name**: Toggle expand/collapse * **Click chevron icon**: Same as clicking the folder name * **Nested folders**: Expand multiple levels to navigate deep structures The file tree remembers which folders you've expanded, even after closing and reopening Glyph. ## File Operations Right-click any file or folder to access context menu operations: ### Creating Files Creates a new markdown note. 1. Right-click a folder 2. Select **New Note** 3. Choose location and filename in the dialog 4. Glyph creates the file with a default heading **Default content:** ```markdown theme={null} # Note Title ``` Creates a new subfolder. 1. Right-click a folder (or empty space for root) 2. Select **New Folder** 3. Glyph creates "New Folder" (or "New Folder 2", etc.) 4. The folder enters rename mode automatically Creates a database view file. 1. Right-click a folder 2. Select **New Database** 3. Glyph creates a database.md file with default config 4. Opens the database view automatically ### Renaming Files and Folders Right-click and select **Rename**, or select the file and press `F2`. Type the new name in the inline editor. Press `Enter` to confirm or `Esc` to cancel. When you rename a note, Glyph automatically updates all wikilinks pointing to that note throughout your space. ### Moving Files Drag and drop files or folders to move them: 1. Click and hold on a file or folder 2. Drag to the target folder 3. Release to complete the move Glyph updates all internal links automatically when you move markdown files. ### Deleting Files and Folders Deletion is permanent. Deleted files are not sent to the system trash. Select the file or folder you want to delete. Choose **Delete** from the context menu. A confirmation dialog appears. Click **Delete** to confirm. Deleting a folder recursively deletes all files and subfolders inside it. ## File Types The file tree displays all files, with special handling for different types: ### Markdown Files * **Icon**: Document icon * **Open behavior**: Opens in the rich markdown editor * **Wikilink support**: Can be referenced with `[[filename]]` * **Search**: Indexed for full-text search ### Database Files * **Icon**: Table/grid icon * **Open behavior**: Opens database view * **Format**: Markdown file with embedded database config ### Other Files * **Images**: `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.webp` * **PDFs**: `.pdf` * **Text files**: `.txt`, `.json`, `.csv`, etc. * **Binary files**: Any other file type Non-markdown files open in a preview pane when clicked. ## Sorting Files and folders are automatically sorted: 1. **Folders first**: All folders appear before files 2. **Alphabetical**: Case-insensitive alphabetical order 3. **Natural sort**: Numbers are sorted numerically (e.g., `note2` before `note10`) ## Empty States When a folder is empty or your space has no files: * The file tree shows "No files found." * You can still right-click to create new files or folders * The message fades in with a subtle animation ## Visual Hierarchy The file tree uses visual cues to show structure: * **Indentation**: Each level is indented 10px * **Connecting lines**: Vertical lines show parent-child relationships * **Chevron icons**: Point right when collapsed, down when expanded * **Hover effects**: Files and folders highlight on hover ## Performance The file tree is optimized for large spaces: * **Lazy loading**: Only loads visible folders * **Virtual rendering**: Efficiently handles thousands of files * **Incremental updates**: Only re-renders changed items * **Debounced file watching**: Batches rapid file system changes # Markdown Editor Source: https://docs.glyphformac.com/features/markdown-editor Rich markdown editing powered by TipTap Glyph uses a powerful TipTap-based editor that supports real-time markdown editing with WYSIWYG formatting. ## Editor Modes Glyph offers three editing modes: **Rich editing with live preview** * Real-time markdown rendering as you type * Formatting toolbar (bottom ribbon) * Slash commands for quick formatting * Wikilink autocomplete * Inline images and embeds This is the default mode for editing notes. **Raw markdown editing** * See the raw markdown syntax * Monospace font * No formatting toolbar * Useful for debugging or precise editing Switch to this mode when you need direct access to the markdown source. **Read-only rendered view** * Fully rendered markdown * No editing capabilities * Clean reading experience * All links are clickable ## Formatting Toolbar When editing in rich mode, hover near the bottom of the editor to reveal the formatting ribbon. ### Format Buttons * **Bold**: `Cmd+B` / `Ctrl+B` * **Italic**: `Cmd+I` / `Ctrl+I` * **Strikethrough**: `Cmd+Shift+X` / `Ctrl+Shift+X` * **Code**: `Cmd+E` / `Ctrl+E` (inline code) * **Link**: Insert markdown links or wikilinks ### Heading Buttons * **H1**: Large section heading * **H2**: Section heading * **H3**: Subsection heading ### List Buttons * **Bullet list**: Unordered list items * **Numbered list**: Ordered list items * **Task list**: Checkboxes with task tracking ### Block Elements * **Quote**: Blockquote * **Code block**: Fenced code blocks with syntax highlighting * **Table**: Markdown tables with header row * **Divider**: Horizontal rule ## Slash Commands Type `/` on a new line to open the slash command menu: Start a new paragraph and type `/` to trigger the menu. Continue typing to filter (e.g., `/h1`, `/table`, `/code`). Use arrow keys to navigate, then press Enter or Tab to insert. ### Available Commands * `/h1`, `/h2`, `/h3` - Insert headings * `/bullet` - Bullet list * `/numbered` - Numbered list * `/quote` - Blockquote * `/code` - Code block * `/table` - Insert 3×3 table * `/divider` - Horizontal rule Slash commands work by keyword too. Try typing `/block` to find blockquote, `/hr` for horizontal rule, or `/ol` for ordered list. ## Markdown Support Glyph supports standard markdown plus GitHub-flavored extensions: ### Text Formatting ```markdown theme={null} **bold** or __bold__ *italic* or _italic_ ~~strikethrough~~ `inline code` ``` ### Headings ```markdown theme={null} # Heading 1 ## Heading 2 ### Heading 3 ``` ### Lists ```markdown theme={null} - Bullet item - Another item 1. Numbered item 2. Second item - [ ] Task item - [x] Completed task ``` ### Links and Images ```markdown theme={null} [Link text](https://example.com) [[Wikilink]] ![Image alt](image.png) ![[Embedded image.png]] ``` ### Code Blocks ````markdown theme={null} ```javascript const greeting = "Hello, world!"; ``` ```` ### Tables ```markdown theme={null} | Column 1 | Column 2 | |----------|----------| | Cell A | Cell B | ``` ### Blockquotes ```markdown theme={null} > This is a quote > spanning multiple lines ``` ### Callouts Glyph supports Obsidian-style callouts: ```markdown theme={null} > [!note] > This is a note callout > [!tip] > Helpful tip here > [!warning] > Important warning ``` Supported callout types: `note`, `tip`, `warning`, `info`, `success`, `error` ## Tasks Task items have special features: ### Task Scheduling When editing a task, click the 📅 icon that appears next to it to set: * **Scheduled date**: When you plan to work on it * **Due date**: When it must be completed Task dates are stored inline: ```markdown theme={null} - [ ] My task 📅 2024-01-20 ⏰ 2024-01-25 ``` ### Task Views View and manage tasks in the Tasks pane, organized by: * **Inbox**: Unscheduled tasks * **Today**: Tasks scheduled for today or overdue * **Upcoming**: Tasks scheduled for future dates ## Images and Attachments Drag and drop images into the editor to automatically: 1. Copy the image to your space's assets folder 2. Insert a markdown image reference 3. Display the image inline (in rich mode) You can also attach files to notes via the attachment commands. ## Keyboard Shortcuts On macOS, use `Cmd` instead of `Ctrl`. | Action | Shortcut | | ------------- | ------------------------------ | | Bold | `Ctrl+B` | | Italic | `Ctrl+I` | | Strikethrough | `Ctrl+Shift+X` | | Inline code | `Ctrl+E` | | Undo | `Ctrl+Z` | | Redo | `Ctrl+Shift+Z` | | Find | `Ctrl+F` | | Save | Auto-save (no shortcut needed) | ## Auto-Save Glyph automatically saves your changes as you type. There's no manual save button - every edit is persisted immediately to disk using atomic file writes to prevent data loss. # Notes Source: https://docs.glyphformac.com/features/notes Create, edit, and organize markdown notes in Glyph Glyph stores all your notes as plain markdown files on your local filesystem, giving you complete ownership and portability of your data. ## Creating Notes You can create new notes in several ways: Right-click any folder in the file tree and select **New Note**. A file dialog will appear where you can name your note. Press `Cmd+N` (macOS) or `Ctrl+N` (Windows/Linux) to create a new note in the current folder. Open the command palette and search for "New Note". All notes are automatically saved as `.md` files. If you don't include the extension, Glyph adds it for you. ## Note Structure Each note can contain: * **Title**: Generated from the filename (without `.md` extension) * **Frontmatter**: YAML metadata block at the top (optional) * **Body**: Your markdown content ### Frontmatter Add structured metadata to your notes using YAML frontmatter: ```markdown theme={null} --- tags: [project, draft] status: in-progress created: 2024-01-15 --- # Your Note Content ``` Frontmatter supports: * **Text properties**: `status: draft` * **Boolean properties**: `published: true` * **List properties**: `tags: [work, important]` * **Date properties**: `created: 2024-01-15` You can click links inside frontmatter - both wikilinks `[[Note]]` and markdown links `[text](url)` work. ## Managing Notes ### Renaming Notes Find the note in your file tree. Choose **Rename** from the context menu. Type the new name and press Enter. Links to this note are updated automatically. ### Moving Notes Drag and drop notes between folders in the file tree. All wikilinks pointing to the note are updated to reflect the new location. ### Deleting Notes Right-click a note and select **Delete**. A confirmation dialog will appear before permanent deletion. Deleted notes cannot be recovered unless you have a backup. Consider using version control or regular backups for important notes. ## Note Properties Panel When editing a note with frontmatter in rich mode, Glyph displays a properties panel above your content. You can: * Edit property values inline * Add new properties * Remove existing properties * Toggle between structured view and raw YAML editor The properties panel automatically syncs with your frontmatter YAML. ## File Storage Notes are stored as plain text markdown files in your space folder: * **Location**: Wherever you created your Glyph space * **Format**: Standard markdown (`.md` files) * **Encoding**: UTF-8 * **Newlines**: Preserved as-is You can edit these files in any text editor, use git for version control, or sync them with any file sync service. ## Backlinks When viewing a note, Glyph automatically displays all other notes that link to it at the bottom of the editor. This creates a bidirectional link graph without requiring manual maintenance. See [Wikilinks](/features/wikilinks) for more details on linking between notes. # Tags & Search Source: https://docs.glyphformac.com/features/tags-search Find notes instantly with full-text search and tag filtering Glyph provides powerful search capabilities backed by SQLite full-text indexing, making it easy to find any note regardless of how large your space grows. ## Full-Text Search ### Opening Search Access search in multiple ways: Press `Cmd+P` (macOS) or `Ctrl+P` (Windows/Linux) to open the command palette with search. Click the search icon in the toolbar. Open the palette and start typing to search. ### Search Behavior When you type a search query: 1. **Debounced**: Waits 180ms after you stop typing 2. **Full-text**: Searches note titles and content 3. **Ranked results**: Most relevant notes appear first 4. **Live updates**: Results refresh as you type Search queries are case-insensitive and support partial word matching. ## Search Syntax ### Basic Search Type any word or phrase: ``` project alpha ``` Finds notes containing both "project" and "alpha". ### Phrase Search Use quotes for exact phrases: ``` "user interface design" ``` Finds notes with that exact phrase. ### Keyword Matching Glyph tokenizes your query and searches for: * **Individual terms**: Each word is a search term * **Minimum length**: Terms must be 2+ characters * **Stopword filtering**: Common words like "the", "a" may be filtered ## Search Results Results display: * **Note title**: Name of the matching note * **Snippet**: Preview of matching content with highlights * **Relevance score**: Higher scores appear first * **Match context**: Surrounding text for context Search results appear as you type, ordered by relevance. Use arrow keys to move between results. Press Enter or click a result to open that note. ## Ranking Algorithm Glyph uses a hybrid search algorithm combining: ### Keyword Overlap * Counts how many search terms appear in the note * Higher overlap = higher score ### Trigram Similarity * Compares character trigrams between query and content * Handles typos and fuzzy matching ### Phrase Bonus * If the exact query phrase appears, boost the score ### Title Bonus * Notes with query terms in the title rank higher **Formula:** ``` score = (0.6 × overlap) + (0.4 × trigram) + phrase_bonus + title_bonus ``` ## Tags ### Tag Syntax Add tags anywhere in your notes: ```markdown theme={null} # Project Notes #project #important #2024 This note contains three tags. ``` Tags must: * Start with `#` * Contain alphanumeric characters, dashes, underscores * Not contain spaces (use `-` instead: `#project-alpha`) ### Nested Tags Create hierarchies with `/`: ```markdown theme={null} #projects/alpha #projects/beta #work/client/acme ``` Nested tags help organize large tag sets. Searching for `#projects` can include all nested tags. ### Frontmatter Tags Define tags in YAML frontmatter: ```markdown theme={null} --- tags: [project, draft, urgent] --- # Note Content ``` Both inline tags and frontmatter tags are indexed identically. ## Tag-Based Search ### Filtering by Tag Use the tag filter in the search interface: 1. Open search 2. Select one or more tags 3. Results narrow to notes with those tags 4. Optionally add a text query ### Tag Autocomplete When typing tags, Glyph suggests: * Existing tags in your space * Tag counts (how many notes use each tag) * Nested tag paths ### Multiple Tags Select multiple tags to find notes with **all** selected tags (AND logic): * Tag `#project` AND `#urgent` * Shows only notes containing both tags ## Combined Search Combine text search with tag filtering: ``` Query: "user interface" Tags: #project, #draft ``` Finds notes that: 1. Contain the phrase "user interface" 2. Have both `#project` and `#draft` tags ## Tag Browser View all tags in your space: Click the Tags icon in the sidebar or press the tags shortcut. See all tags with usage counts. View all notes with that tag. The tags list shows: * **Tag name**: The full tag text * **Count**: Number of notes using this tag * **Sorted**: By count (most used first) or alphabetically ## Advanced Search Glyph supports advanced search parameters via the Tauri command interface: ### Search Options * **`query`**: Text to search for * **`tags`**: Array of tags to filter by * **`title_only`**: Search only note titles * **`tag_only`**: Search only tags (no content) * **`limit`**: Maximum results (default: 50) ### Example Searches Search only in note titles, not content: ```typescript theme={null} invoke("search_advanced", { request: { query: "meeting", title_only: true } }); ``` Find notes by tag without text search: ```typescript theme={null} invoke("search_with_tags", { tags: ["project", "active"] }); ``` Text search + tag filter + limit: ```typescript theme={null} invoke("search_advanced", { request: { query: "design system", tags: ["ui", "reference"], limit: 20 } }); ``` ## Indexing ### Automatic Indexing Glyph indexes your notes automatically: * **On save**: Every time you save a note * **On create**: When you create a new note * **On delete**: When you delete a note * **On rename**: When you rename a note ### Index Contents The search index stores: * **Note title**: Filename without extension * **Note content**: Full markdown text * **Tags**: Both inline and frontmatter tags * **Paths**: Relative path in your space * **Timestamps**: Created and updated times ### Rebuild Index If search results seem outdated: Go to Settings > General. Click **Rebuild Search Index**. Glyph re-indexes all notes. Large spaces may take a minute. Rebuilding the index locks search temporarily. Avoid doing this during active work sessions. ## Recent Notes View recently modified notes: ```typescript theme={null} invoke("recent_notes", { limit: 10 }); ``` This returns the 10 most recently updated notes, useful for: * Quick access to recent work * "Continue where you left off" features * Recent activity timeline ## Performance ### Search Speed Glyph's search is optimized for spaces with thousands of notes: * **SQLite FTS5**: Full-text search engine * **Indexed queries**: Sub-100ms for most queries * **Candidate limiting**: Fetches top 300 candidates, then ranks * **Debounced input**: Reduces unnecessary queries ### Scaling Search performance characteristics: | Space Size | Index Time | Query Time | | ------------ | ---------- | ---------- | | 100 notes | \< 1s | \< 10ms | | 1,000 notes | \~5s | \~50ms | | 10,000 notes | \~30s | \~100ms | Search performance depends on note size. Many small notes search faster than fewer large notes. ## Troubleshooting ### Search returns no results **Solutions:** 1. Check spelling and try synonyms 2. Simplify your query (use fewer words) 3. Rebuild the search index 4. Verify the note exists in your space ### Search is slow **Solutions:** 1. Reduce query complexity 2. Use more specific search terms 3. Filter by tags to narrow results 4. Rebuild index if it's corrupted ### Tags not appearing **Solutions:** 1. Ensure tags start with `#` 2. Check for spaces (use `#tag-name` not `#tag name`) 3. Save the note (tags index on save) 4. Rebuild index if needed # Task Management Source: https://docs.glyphformac.com/features/task-management Track and organize tasks across all your notes with three focused views Glyph's task management system extracts checkboxes from your markdown notes and organizes them into three views: **Inbox**, **Today**, and **Upcoming**. Tasks stay embedded in your notes while appearing in these aggregated views. ## Overview Tasks are standard markdown checkboxes: ```markdown theme={null} - [ ] Review quarterly goals - [x] Submit expense report - [ ] Call vendor about renewal ``` Glyph automatically indexes these and makes them queryable across your entire space. Task pane showing inbox, today, and upcoming views ## Task Views ### Inbox The inbox shows **unscheduled tasks** grouped by their source note: Tasks are organized under their parent note with the note title and path. Click the note header to jump to that note in your editor. Schedule tasks for today or upcoming dates directly from the inbox. **Use inbox for:** * Newly captured tasks without dates * Weekly reviews to schedule work * Seeing all open tasks in context ### Today The today view displays a **flat list** of tasks scheduled or due today: * Tasks with `scheduled: YYYY-MM-DD` matching today * Tasks with `due: YYYY-MM-DD` matching today * Sorted by priority and note update time * No grouping—pure action list **Use today for:** * Daily planning sessions * Focused execution view * Clearing today's commitments ### Upcoming The upcoming view shows **future tasks** sorted by their next relevant date: * Scheduled tasks appear by scheduled date * If no scheduled date, sorted by due date * Helps you plan ahead and prevent surprises * Flat list sorted chronologically **Use upcoming for:** * Weekly planning * Spotting date conflicts * Balancing workload across days Unscheduled tasks grouped by note for triage Work scheduled or due today in a focused list Future tasks sorted by their next date ## Task Syntax Tasks use standard markdown checkboxes with optional inline metadata: ### Basic Checkbox ```markdown theme={null} - [ ] This is a task - [x] This task is complete ``` ### Task Status Use alternate checkbox characters for custom statuses: ```markdown theme={null} - [ ] Todo - [x] Done - [-] Cancelled - [>] Forwarded - [<] Scheduled ``` Glyph tracks the status character but primarily uses checked/unchecked state. ### Inline Dates Add dates with special syntax anywhere in the task text: ```markdown theme={null} - [ ] Review proposal 📅 2024-03-15 - [ ] Call client ⏳ 2024-03-10 📅 2024-03-12 ``` * `📅 YYYY-MM-DD` or `due: YYYY-MM-DD` sets the due date * `⏳ YYYY-MM-DD` or `scheduled: YYYY-MM-DD` sets the scheduled date Both emoji and text formats work. Use what fits your workflow. ### Priority Add priority indicators: ```markdown theme={null} - [ ] High priority task ⬆️ - [ ] Low priority task ⬇️ - [ ] Normal priority task ``` Priority affects sort order within the same date bucket. ### Sections Tasks inherit the section (heading) they appear under: ```markdown theme={null} ## Q1 Objectives - [ ] Launch new feature - [ ] Hire engineer ## Q2 Objectives - [ ] Expand to EMEA ``` The task object includes the section name for context. ## Using the Task Pane ### Opening the Pane Access tasks from: * Sidebar navigation button * Command palette (search "Tasks") * Keyboard shortcut (configurable) ### Switching Views Three pills at the top switch between inbox, today, and upcoming: * Click a pill to activate that view * Active view is highlighted * Badge shows task count for current view ### Task Actions Click the checkbox to toggle completion state. Changes save immediately to the source note. Click the date icon to set or change scheduled/due dates via a date picker. Click the task text or note header to navigate to that note in the editor. ### Real-time Sync Tasks update automatically: * Checking a task updates the markdown file * Editing a task in the note updates the pane * Adding new tasks shows them immediately * Deleting tasks removes them from the pane ## Task Row Layout Each task row displays: 1. **Checkbox**: Click to toggle completion 2. **Task text**: The markdown content (without date metadata) 3. **Date badge**: Shows scheduled or due date if set 4. **Note path**: (Today/Upcoming only) Shows source note 5. **Section**: Parent heading context if available ### Task Row Example ``` ┌─ [ ] Review quarterly goals 📅 Mar 15 │ projects/2024/strategy.md · Q1 Planning └─ ``` * First line: checkbox + task text + date * Second line: note path + section ## Scheduling Tasks ### From the Task Pane Each task row has a calendar icon on the right. Use the date picker to select a scheduled or due date. Glyph updates the task's markdown line with the new date metadata. ### In the Editor Add dates directly while writing: ```markdown theme={null} - [ ] Call vendor scheduled: 2024-03-20 - [ ] Submit report due: 2024-03-18 ``` The task pane updates immediately when you save. ### Scheduled vs Due * **Scheduled date**: When you plan to work on it * **Due date**: External deadline * Tasks can have both, one, or neither **Bucketing logic:** * **Today**: Scheduled = today OR due = today * **Upcoming**: Scheduled > today OR (no scheduled AND due > today) * **Inbox**: No scheduled AND no due ## Task Grouping ### Inbox Grouping Inbox tasks group by their source note: ``` 📄 projects/client-work.md - [ ] Send proposal - [ ] Schedule kickoff 📄 personal/home.md - [ ] Fix leaky faucet - [ ] Call electrician ``` Click the note header to open that note. ### Flat Views Today and Upcoming use flat lists sorted by: 1. **Date** (scheduled or due) 2. **Priority** (high → normal → low) 3. **Note updated time** (most recent first) No grouping allows laser focus on chronological order. ## Filtering Tasks Configure task sources in Settings → Tasks: ### Source Modes Index tasks from every note in your space. Choose which folders to include. Tasks from other folders won't appear. ### Example: Work/Personal Split Create two Glyph spaces: * **Work space**: All task folders enabled * **Personal space**: Only personal folders enabled Or use a single space with folder filtering to separate contexts. ## Task Indexing Glyph uses SQLite to index tasks: * **Automatic**: New tasks appear within seconds * **Incremental**: Only changed notes are re-indexed * **Efficient**: Handles thousands of tasks across hundreds of notes ### Manual Rebuild If tasks seem out of sync: 1. Open Settings → General 2. Click "Rebuild Index" 3. Wait for completion This re-scans all notes and rebuilds the task database. ## Task Properties Each task object includes: | Property | Description | | ---------------- | ------------------------------------------- | | `task_id` | Unique identifier - note path + line number | | `note_id` | Parent note identifier | | `note_title` | Title of the source note | | `note_path` | Relative path to the note | | `line_start` | Line number in the markdown file | | `raw_text` | Full markdown line | | `checked` | Boolean completion state | | `status` | Checkbox character: space, x, -, >, or \< | | `priority` | Integer: -1 low, 0 normal, 1 high | | `due_date` | ISO date string or null | | `scheduled_date` | ISO date string or null | | `section` | Parent heading text or null | | `note_updated` | Note's last modified timestamp | ## Keyboard Shortcuts | Shortcut | Action | | ----------------- | ----------------- | | Click checkbox | Toggle completion | | Click task text | Open source note | | Click date icon | Edit dates | | Click note header | Navigate to note | | Refresh button | Reload tasks | ## Best Practices 1. **Use inbox for capture** - Write tasks in notes, triage from inbox 2. **Schedule daily** - Start each day by reviewing today view 3. **Plan weekly** - Use upcoming to balance your week 4. **Keep tasks in context** - Don't extract tasks to separate files 5. **Leverage sections** - Use headings to add context to task groups 6. **Set realistic dates** - Only schedule what you can complete 7. **Review regularly** - Weekly inbox zero keeps tasks manageable ## Common Workflows ### Daily Planning Open inbox view and schedule high-priority tasks for today. Confirm today's list is achievable. Check off tasks as you complete them. Reschedule incomplete tasks to tomorrow or upcoming. ### Weekly Review Schedule all inbox tasks or delete ones no longer relevant. Ensure next week's tasks are realistic and prioritized. Reschedule or complete tasks with past due dates. ### Project Management ```markdown theme={null} # Project: Website Redesign ## Planning - [ ] Wireframe homepage scheduled: 2024-03-10 - [ ] Review with team due: 2024-03-12 ## Design - [ ] Create mockups scheduled: 2024-03-15 - [ ] Get client feedback due: 2024-03-18 ## Development - [ ] Build prototype scheduled: 2024-03-20 - [ ] Deploy to staging due: 2024-03-25 ``` Tasks appear in the task pane organized by date, while remaining in the project note. ## Integration with Notes Tasks stay in your notes where they belong: * **Meeting notes**: Action items from discussions * **Project plans**: Milestones and deliverables * **Daily notes**: Today's tasks and intentions * **Research**: Next steps and follow-ups The task pane is a **view**, not a separate system. ## Limitations * Tasks must be in markdown files (not other formats) * Only checkboxes at the start of list items are recognized * Nested tasks (sub-items) are treated as independent tasks * No recurring task syntax (yet) * Maximum 2000 tasks per view (configurable) ## Related Features * [Daily Notes](/features/daily-notes) - Template for daily task planning * [Tags & Search](/features/tags-search) - Find tasks by content * [Database Views](/features/databases) - Organize notes with tasks * [Markdown Editor](/features/markdown-editor) - Edit tasks inline in your notes # Wikilinks Source: https://docs.glyphformac.com/features/wikilinks Link notes together with wikilinks and discover backlinks automatically Wikilinks are the primary way to create connections between notes in Glyph. They're simple, autocomplete-enabled, and automatically maintain bidirectional relationships. ## Wikilink Syntax Wikilinks use double square brackets: ```markdown theme={null} [[Note Name]] ``` This creates a link to the note at `Note Name.md` in your space. ## Creating Wikilinks Type `[[` to start a wikilink. Glyph shows an autocomplete menu with matching notes: 1. Type `[[` 2. Start typing the note name 3. Use arrow keys to navigate suggestions 4. Press `Enter` or `Tab` to select 5. Type `]]` to close (or it's added automatically) The autocomplete menu shows: * **Note title**: The display name * **Note path**: Full path in your space * **Live filtering**: Results update as you type Press `Esc` to dismiss the menu without inserting. Paste wikilink syntax directly: ```markdown theme={null} I copied this from another note: [[Project Notes]] ``` Glyph parses and renders it automatically. ## Wikilink Features ### Display Names (Aliases) Show different text than the note name: ```markdown theme={null} [[Actual Note Name|Display Text]] ``` **Example:** ```markdown theme={null} Read more in [[2024-01-15|yesterday's note]]. ``` Displays as: "Read more in yesterday's note" but links to `2024-01-15.md`. ### Headings and Anchors Link to specific sections within a note: ```markdown theme={null} [[Note Name#Heading]] ``` **Example:** ```markdown theme={null} See [[Project Plan#Timeline]] for deadlines. ``` Clicking this link opens the note and scrolls to the "Timeline" heading. ### Combining Alias and Anchor ```markdown theme={null} [[Note Name#Heading|Custom Display]] ``` **Example:** ```markdown theme={null} Check [[Meeting Notes 2024-01-15#Action Items|today's action items]]. ``` ### Block References Link to specific blocks (paragraphs) using `^`: ```markdown theme={null} [[Note Name#^block-id]] ``` Block IDs are typically user-defined anchors in the target note. ## Embedded Images Use `!` prefix to embed images: ```markdown theme={null} ![[diagram.png]] ``` This displays the image inline instead of showing a link. Works with: * `.png`, `.jpg`, `.jpeg` * `.webp`, `.gif`, `.svg` * `.bmp`, `.avif` * `.tif`, `.tiff` You can also add an alias to embedded images: ```markdown theme={null} ![[screenshot.png|Architecture diagram]] ``` ## Link Resolution Glyph resolves wikilinks intelligently: ### By Filename ```markdown theme={null} [[Note]] ``` Finds `Note.md` anywhere in your space. ### With Path ```markdown theme={null} [[folder/subfolder/Note]] ``` Links to a note at that specific path. ### Ambiguity Handling If multiple notes have the same name: * Glyph prefers the note closest to the current note's folder * Use a path to disambiguate: `[[projects/Meeting Notes]]` Wikilinks are case-insensitive. `[[Note]]`, `[[note]]`, and `[[NOTE]]` all link to `Note.md`. ## Unresolved Links When a wikilink points to a non-existent note: * It's styled differently (typically dimmed or marked) * The `unresolved` attribute is set to `true` * Clicking it can create the note (if your workflow supports it) This lets you create links first and fill in content later. ## Backlinks Backlinks are automatically discovered and displayed: ### How Backlinks Work 1. You create a link from Note A to Note B: `[[Note B]]` 2. Glyph indexes this relationship 3. When viewing Note B, Glyph shows "Linked mentions" at the bottom 4. The backlinks section lists all notes that link to Note B ### Backlinks Display Backlinks appear at the bottom of the editor: ``` Linked mentions (3) - Project Overview - Meeting Notes 2024-01-15 - Research Findings ``` Click any backlink to navigate to that note. Backlinks are bidirectional but only require one-way linking. You don't need to manually maintain links in both directions. ## Search and Autocomplete The wikilink autocomplete searches: * **Note titles**: Matches against the filename (without `.md`) * **Note paths**: Full relative path in your space * **Fuzzy matching**: `proj notes` finds "Project Notes" * **Limit**: Shows up to 8 results by default ### Autocomplete Behavior * **Opens on `[[`**: Menu appears immediately * **Filters as you type**: Results narrow down * **Keyboard navigation**: Arrow keys to move, Enter/Tab to select * **Mouse selection**: Click any item to insert * **Dismiss**: Press `Esc` or click outside ## Link Updates When you rename or move a note, Glyph automatically: 1. Finds all wikilinks pointing to the old location 2. Updates them to point to the new location 3. Preserves aliases and anchors 4. Re-indexes backlinks This ensures your link graph stays intact as your space evolves. ## Markdown Links vs Wikilinks Glyph supports both syntaxes: **Syntax**: `[[Note Name]]` **Pros:** * Shorter syntax * Autocomplete built-in * Automatic link updates on rename * Backlinks indexed **Use for:** Internal note-to-note links **Syntax**: `[Text](path/to/note.md)` **Pros:** * Standard markdown * Works in other markdown tools * Can use relative paths * Can link to external URLs **Use for:** External links, compatibility, precise paths Both link types work in Glyph. Use wikilinks for internal notes and markdown links for external URLs or when you need standard markdown compatibility. ## Working with Wikilinks ### Building a Knowledge Graph Create connections as you write: ```markdown theme={null} # Project Alpha This project relates to [[Strategy 2024]] and builds on work from [[Previous Project]]. Key stakeholders: [[Team Directory]] ``` Over time, your notes form an interconnected graph. ### MOCs (Maps of Content) Create index notes with many wikilinks: ```markdown theme={null} # Web Development MOC ## Frameworks - [[React Patterns]] - [[Vue Best Practices]] - [[Svelte Guide]] ## Tools - [[VS Code Setup]] - [[Git Workflow]] ``` ### Daily Note Links Reference daily notes chronologically: ```markdown theme={null} # 2024-01-15 Continued from [[2024-01-14]] Planning for [[2024-01-16]] ``` ## Technical Details ### Link Indexing Glyph uses SQLite to index wikilinks: * **Real-time indexing**: Links are indexed as you type * **Fast lookups**: Backlinks load instantly * **Incremental updates**: Only changed notes re-index ### Link Storage Wikilinks are stored as plain text in markdown files: * **Format**: `[[target]]` or `[[target|alias]]` * **Encoding**: UTF-8 * **Preservation**: Original syntax is never modified (except during rename) ### Performance Wikilink autocomplete is optimized for large spaces: * **Debounced queries**: Waits for typing to pause * **Indexed search**: Uses SQLite FTS for speed * **Result limiting**: Shows top 8 matches # Installation Source: https://docs.glyphformac.com/installation Download and install Glyph on macOS, Windows, or Linux. Get started with the 48-hour free trial. # Installation Glyph is available for macOS, Windows, and Linux. Official release binaries include a 48-hour free trial, with optional license activation for continued use. **Open Source:** Glyph's source code is public on GitHub. Development builds are free and unlimited—the trial only applies to official release binaries. ## Download Glyph Download official binaries (free 48-hour trial) Buy a lifetime license on Gumroad ## Platform-Specific Installation ### macOS Installation 1. Visit [GitHub Releases](https://github.com/SidhuK/Glyph/releases) 2. Download the latest `.dmg` or `.app` file for macOS 1. Open the downloaded `.dmg` file (if applicable) 2. Drag `Glyph.app` to your Applications folder Glyph is currently distributed as an unsigned macOS app. On first launch, macOS may block it with a message like: > "Glyph can't be opened because Apple cannot check it for malicious software." This is expected. To open Glyph: **Method 1: System Settings** 1. Try opening Glyph once (it will be blocked) 2. Open **System Settings** → **Privacy & Security** 3. Scroll to the security section 4. Click **Open Anyway** next to Glyph 5. Confirm by clicking **Open** in the dialog **Method 2: Finder** 1. In Finder, right-click `Glyph.app` 2. Click **Open** 3. Click **Open** again in the warning dialog After this one-time approval, Glyph will open normally. You only need to do this once. ### Why is Glyph unsigned? Glyph is not Apple-notarized or code-signed. This is a common approach for open source desktop apps distributed outside the Mac App Store. * **Security:** Review the [source code](https://github.com/SidhuK/Glyph) yourself * **Future:** Code signing may be added in future releases * **Alternatives:** Build from source for complete control ### Windows Installation 1. Visit [GitHub Releases](https://github.com/SidhuK/Glyph/releases) 2. Download the latest `.exe` or `.msi` installer for Windows 1. Run the downloaded installer 2. Follow the installation wizard 3. Choose installation location (default is recommended) 1. Launch Glyph from the Start menu or desktop shortcut 2. Windows may show a SmartScreen warning for unsigned apps 3. Click **More info** → **Run anyway** if prompted Glyph stores its data in the folder you select as your "space." No data is written to system directories without your permission. ### Linux Installation 1. Visit [GitHub Releases](https://github.com/SidhuK/Glyph/releases) 2. Download the appropriate package: * `.AppImage` - Universal, no installation needed * `.deb` - Debian/Ubuntu-based distros * `.rpm` - Fedora/RHEL-based distros **AppImage (Universal)** ```bash theme={null} chmod +x Glyph.AppImage ./Glyph.AppImage ``` **Debian/Ubuntu (.deb)** ```bash theme={null} sudo dpkg -i glyph_*.deb sudo apt-get install -f # Install dependencies if needed ``` **Fedora/RHEL (.rpm)** ```bash theme={null} sudo rpm -i glyph-*.rpm ``` * AppImage: Run the `.AppImage` file directly * Installed packages: Run `glyph` from your application launcher or terminal ## First Launch & Setup When you first open Glyph, you'll see an animated welcome screen introducing the app. The welcome flow shows: 1. **Hero** - Introduction to Glyph 2. **Quick Tips** - Three key concepts (open folder, browse files, find fast) 3. **Get Started** - Create or open your first space A **space** is a folder where Glyph stores your notes. Choose one: * **Create New Space** - Glyph creates a new folder * **Open Existing Folder** - Use a folder that already contains markdown files * **Continue Last Space** - Return to your most recent workspace You can have multiple spaces for different projects. Switch between them using the command palette (`Cmd+K` or `Ctrl+K`). Once your space is open, you're ready to go! * Create your first note * Explore the file tree * Try the command palette (`Cmd+K` / `Ctrl+K`) See the [Quickstart Guide](/quickstart) for a detailed walkthrough. ## Trial & Licensing ### 48-Hour Free Trial Official release binaries include a **48-hour free trial**: * Trial starts on first launch * Full access to all features during trial * Trial countdown shown in a banner * No credit card required The trial timer is stored locally and tied to your installation. Development builds (built from source) have no trial or licensing. ### After the Trial When the trial expires: 1. Glyph shows a lock screen with activation options 2. You can: * **Enter a license key** (purchased from Gumroad) * **Purchase a license** (opens Gumroad in browser) ### License Activation Buy Glyph on [Gumroad](https://karatsidhu.gumroad.com/l/sqxfay): * **One-time purchase** * **Lifetime access** * **Unlimited devices** * **No subscription** After purchase, you'll receive a license key via email. **Option 1: During Trial** * Click **Enter License Key** in the trial banner * Paste your key and click **Activate** **Option 2: After Trial Expires** * Enter your key on the lock screen * Click **Activate** **Option 3: Settings** * Open Settings (`Cmd+,` or `Ctrl+,`) * Go to **General** → **License** * Click **Enter License Key** Glyph verifies your license with Gumroad: * **Online verification** required once * **Offline use** after successful activation * License stored locally (encrypted) * No periodic re-checks Glyph needs internet access **once** to verify your license. After that, it works completely offline. ### License Details From the official licensing documentation: * **One-time purchase** - Pay once, use forever * **Lifetime access** - No expiration * **Unlimited devices** - Install on all your computers * **No seat limits** - Use across work and personal machines * **No device binding** - Transfer between devices freely * **Offline use** - No periodic online re-checks after activation These remain free and unrestricted: * **Source code** - Always public on GitHub * **Development builds** - Build from source yourself * **Community builds** - Self-compiled binaries The license only applies to **official release binaries** distributed through GitHub Releases. ## Building from Source If you prefer to build Glyph yourself (no trial, no licensing): Install required tools: * **Node.js** 18+ and **pnpm** * **Rust** (latest stable) * **Tauri CLI** dependencies for your platform See [Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites) for platform-specific setup. ```bash theme={null} git clone https://github.com/SidhuK/Glyph.git cd Glyph pnpm install pnpm tauri build ``` The built app will be in `src-tauri/target/release/bundle/`. For development with hot-reload: ```bash theme={null} pnpm tauri dev ``` Development builds use the environment variable `GLYPH_OFFICIAL_BUILD=1` to determine if licensing should be enforced. Local builds default to unlimited use. ## Automatic Updates Glyph includes an automatic updater powered by Tauri: * Checks for updates on launch * Downloads updates in the background * Prompts you to install when ready * Updates are signed and verified The updater uses GitHub Releases as the update source. Both free and licensed users receive updates the same way. ## Troubleshooting See the [macOS installation instructions](#macos) above. You need to approve Glyph once in System Settings → Privacy & Security. Click **More info** → **Run anyway**. This is normal for apps not distributed through the Microsoft Store. Common issues: * **No internet connection** - Glyph needs internet for initial activation * **Invalid key** - Double-check the key from your Gumroad purchase email * **Already activated** - Each key can be used on unlimited devices If problems persist, contact support via [GitHub Issues](https://github.com/SidhuK/Glyph/issues). * Ensure you have write permissions to the folder * Try creating a new folder in a location you own (e.g., Documents) * Check that the folder path doesn't contain special characters ## System Requirements ### Minimum Requirements * **macOS:** 10.15 (Catalina) or later * **Windows:** Windows 10 or later * **Linux:** Modern distro with GTK 3.24+ ### Recommended * 4 GB RAM (8 GB+ for AI features with Ollama) * 500 MB free disk space * Internet connection for: * Initial license activation * AI providers (OpenAI, Anthropic, Gemini) * Automatic updates **Offline use:** After initial setup, Glyph works completely offline. Use Ollama for local AI with no internet required. ## Data & Privacy Glyph is **offline-first** and **local-first**: * All notes stored as plain markdown files on your device * Search index stored in `.glyph/index.db` within your space * No cloud sync or telemetry * AI requests go directly to your chosen provider (if you configure one) * License verification requires one internet check, then works offline Your data never touches Glyph's servers. The only network requests are license verification (one-time) and AI API calls (if you enable them). ## Support & Community Learn how to use Glyph in 5 minutes Report bugs or request features Explore the codebase Get help with licensing *** Follow the quickstart guide # Introduction Source: https://docs.glyphformac.com/introduction Glyph is an offline-first desktop notes app that helps you think, capture ideas, and chat with AI—all in one local workspace. # Welcome to Glyph Glyph is an offline-first desktop notes app built with Tauri, React, and TypeScript. It combines powerful markdown editing, AI chat capabilities, and local-first data storage to create a simple space for your notes and ideas. Get up and running in 5 minutes Download and install Glyph on your system Explore what makes Glyph powerful View source code and contribute ## What is Glyph? Glyph is designed for people who want a fast, offline-first notes app that doesn't compromise on features. Your notes live in a local folder as plain markdown files, giving you full ownership and portability. **Offline-first:** All your data stays on your device. No cloud sync, no servers, no vendor lock-in. Just you and your files. ## Key Features ### Markdown-First Editing Glyph uses a powerful TipTap-based markdown editor that supports: * Standard markdown syntax * Wikilinks for connecting notes (`[[note-name]]`) * Slash commands for quick formatting * Task lists with checkboxes * Frontmatter metadata ### AI Chat Integration Chat with AI models directly from your notes sidebar. Glyph supports multiple AI providers: * **OpenAI** - GPT-4, GPT-3.5 * **Anthropic** - Claude models * **Google Gemini** - Gemini Pro and Flash * **Ollama** - Local models * **OpenRouter** - Access to multiple models * **OpenAI-compatible** - Custom endpoints The AI panel includes: * Multi-turn conversations with context * Chat history and session management * Multiple AI profiles for different use cases * Attach files and folders as context ```typescript theme={null} // From src/components/ai/AIPanel.tsx:111 void chat.sendMessage( { text: trimmed }, { body: { profile_id: profiles.activeProfileId ?? undefined, provider: activeProvider, mode: aiAssistantMode, context: built.payload || undefined, context_manifest: built.manifest ?? undefined, audit: true, }, }, ); ``` ### Daily Notes Create a note for each day automatically. Glyph generates daily notes with YYYY-MM-DD naming: ```typescript theme={null} // From src/lib/dailyNotes.ts:13 export function getDailyNoteFilename(date?: string): string { const d = date ?? getTodayDateString(); return `${d}.md`; // e.g., "2026-03-03.md" } ``` Configure your daily notes folder in Settings → Daily Notes. ### Tasks & Databases Glyph indexes your markdown task lists and provides a unified tasks view: * Checkbox syntax: `- [ ] task` and `- [x] completed` * Task filtering and search * View tasks across all notes * Task completion tracking ### Fast Search Hybrid search powered by SQLite indexing: * Full-text search across all notes * Tag search and filtering * Link graph exploration * Frontmatter metadata queries Search uses a hybrid index stored in `.glyph/` within your space folder, combining full-text and metadata indexing. ### Wikilinks & Backlinks Connect your notes with wikilinks: * `[[Note Title]]` - Link to another note * Automatic backlink detection * Link graph for exploring connections * Works with partial matches ## Architecture * **Framework:** React 19 + Vite + TypeScript * **UI:** shadcn/ui + Radix + Tailwind 4 * **Editor:** TipTap (ProseMirror) * **State:** React Context API * **Animation:** Motion (Framer Motion) * **Framework:** Tauri 2 * **Database:** SQLite (via rusqlite) * **AI Runtime:** Rig framework * **File watching:** notify-rs * **Storage:** Local filesystem + `.glyph/` metadata ## Local-First Philosophy Glyph stores everything locally: * **Notes:** Plain markdown files in your chosen folder * **Index:** SQLite database in `.glyph/index.db` * **Settings:** Per-space configuration in `.glyph/` * **No cloud:** Your data never leaves your device (unless you choose to sync the folder yourself) ## Open Source Glyph is open source on GitHub. Official release binaries include a 48-hour free trial with optional Gumroad license activation. Get the latest release Support development on Gumroad **Development builds** are free and unlimited. The trial and licensing only apply to official release binaries. ## Next Steps [Download and install](/installation) Glyph for your platform Follow the [quickstart guide](/quickstart) to set up your workspace Create your first note and explore the editor *** Jump into the quickstart guide # License Activation Source: https://docs.glyphformac.com/licensing/activation How to activate your Glyph license key After purchasing Glyph from Gumroad, you'll receive a license key that unlocks the app on all your devices. ## Activation Process Buy Glyph from [Gumroad](https://karatsidhu.gumroad.com/l/sqxfay). You'll receive your license key immediately via email. Launch the official Glyph binary you downloaded from [GitHub Releases](https://github.com/SidhuK/Glyph/releases). You can activate through any of these methods: * Click **Enter License Key** in the trial banner (during trial) * Use the lock screen after trial expiration * Navigate to **Settings → General → License** * Use command palette: `Manage license` Glyph sends your key to Gumroad's verification endpoint: ``` POST https://api.gumroad.com/v2/licenses/verify ``` This requires an internet connection for the first activation only. Once verified, Glyph stores a local activation record and unlocks permanently. You can now use Glyph offline forever on this device. ## Verification Flow Glyph verifies license keys directly against Gumroad using the product ID configured for official builds: * **Verification endpoint**: `POST https://api.gumroad.com/v2/licenses/verify` * **Required fields**: `product_id` and `license_key` * **Success rule**: Activation succeeds only if Gumroad returns `success: true` ## What Gets Stored Locally Glyph **does not** store your raw license key on disk. Instead, it stores: * **Masked license key** (e.g., `ABCD-****-****-WXYZ`) for display only * **Key hash** (SHA-256) for verification * **Activation timestamp** when you first activated * **Trial timestamps** (if you started a trial first) This data is stored in: ``` app_config_dir()/license.json ``` Your raw license key is never written to disk for security reasons. ## Offline Behavior After Gumroad verifies your key once: 1. Glyph stores a local activation record 2. Glyph continues working **offline forever** on that installation 3. No periodic online rechecks are required 4. You can reinstall on unlimited devices This is an honest-user licensing model, not DRM. If you modify or delete the license file, you'll need to reactivate with internet access. ## Multiple Devices Your Glyph license has: * **No device limits** - Install on as many computers as you need * **No seat counting** - Use simultaneously on multiple machines * **No device fingerprinting** - Hardware changes won't invalidate your license * **No activation caps** - Reinstall unlimited times ## Activation from Settings If you're already using Glyph, you can activate from Settings: 1. Open **Settings → General → License** 2. View your current license status (Trial Active, Trial Expired, Licensed, Community Build) 3. Enter your license key in the input field 4. Click **Activate** 5. Wait for Gumroad verification (requires internet) 6. Success! Glyph is now unlocked ## Removing Local Activation If you need to clear your local activation record: 1. Open **Settings → General → License** 2. Click **Remove Local Activation** 3. Your trial state will be restored (if trial is expired, you'll see the lock screen) 4. You can reactivate with your license key anytime Removing local activation does not invalidate your license key. You can always reactivate with the same key. ## Next Steps * [Trial System](/licensing/trial) - Learn about the 48-hour trial * [Troubleshooting](/licensing/troubleshooting) - Common activation issues * [FAQ](/licensing/faq) - Frequently asked questions # Frequently Asked Questions Source: https://docs.glyphformac.com/licensing/faq Common questions about Glyph licensing and trials Answers to common questions about purchasing, activating, and managing your Glyph license. ## General Questions Yes! Glyph's **source code is public** on [GitHub](https://github.com/SidhuK/Glyph) and licensed under an open source license. However, **official release binaries** downloaded from GitHub Releases require a license after the 48-hour trial period. You can always build Glyph from source yourself without any licensing restrictions. * **Source code**: Remains public and freely accessible on GitHub * **Official binaries**: Pre-built releases that include a 48-hour trial and require a Gumroad license * **Community builds**: Self-built or contributor builds that don't enforce licensing Think of it as "pay for convenience" - the official binaries are professionally built, signed, and auto-updated. Yes, in two ways: 1. **48-hour trial**: Every official binary includes a free trial 2. **Build from source**: Clone the repo and build Glyph yourself - no licensing restrictions For continued use of official binaries, a one-time purchase is required. Glyph is available for a **one-time purchase** on [Gumroad](https://karatsidhu.gumroad.com/l/sqxfay). * **No subscription fees** * **Lifetime access** * **All future updates included** * **Unlimited devices** ## Licensing Questions **Unlimited!** There are no device limits, seat restrictions, or activation caps. Purchase once and use on: * Multiple computers (Mac, Windows, Linux) * Work and personal machines * Unlimited reinstalls No device fingerprinting or hardware binding. **Only once for activation**. Here's how it works: 1. **First activation**: Requires internet to verify with Gumroad 2. **After activation**: Glyph works completely offline forever 3. **No periodic checks**: No "phone home" or online rechecks Your license is validated once and stored locally. Nothing! Your license key works on the new computer too: 1. Download Glyph on your new device 2. Enter the same license key 3. Glyph verifies with Gumroad (internet required) 4. You're activated on the new device Your old device stays activated as well - no limits. Refunds are handled through **Gumroad's policies**. Contact Gumroad support or check your purchase email for refund options. We recommend trying the **48-hour free trial** first to ensure Glyph meets your needs. ## Trial Questions Exactly **48 hours** (2 days) from the moment you first launch an official Glyph binary. The trial starts automatically - no signup or email required. No, the trial is a one-time 48-hour period. However: * You have full access to all features during the trial * You can purchase and activate anytime during or after the trial * Building from source gives you unlimited access without a trial After 48 hours: 1. Glyph shows a **lock screen** requiring activation 2. You cannot access the app or your notes 3. You can enter a license key to unlock 4. Or purchase a license directly from the lock screen Your notes remain safe and accessible once you activate. Each device gets its own 48-hour trial when you first install Glyph. However, if you plan to use Glyph on multiple devices, consider purchasing a license - it works on **unlimited devices** with no restrictions. ## Activation Questions You can activate through any of these methods: * Click **Enter License Key** in the trial banner * Wait for trial expiration and use the lock screen * Go to **Settings → General → License** * Use command palette: `Manage license` 1. You enter your license key from Gumroad 2. Glyph sends it to Gumroad's verification API (requires internet) 3. Gumroad confirms it's valid 4. Glyph stores a local activation record (not the raw key) 5. You're unlocked permanently on that device After first activation, no internet required - works offline forever. **No, your raw license key is never stored.** For security, Glyph only stores: * Masked key (e.g., `ABCD-****-****-WXYZ`) for display * SHA-256 hash of the key for verification * Activation timestamp This is stored in `app_config_dir()/license.json`. Check your **Gumroad purchase email** - your license key is included there. If you can't find it: 1. Log into [Gumroad Library](https://gumroad.com/library) 2. Find your Glyph purchase 3. View your license key For further help, contact Gumroad support. **Not necessary!** Your license has no device limits. You can: * Keep it activated on your old computer * Activate it on your new computer * Use both simultaneously If you want to remove local activation for testing: 1. Go to **Settings → General → License** 2. Click **Remove Local Activation** 3. This doesn't invalidate your key - you can reactivate anytime ## Technical Questions Check in **Settings → General → License**: * **Official Release**: Shows license status and trial info * **Community Build**: Shows "Community Build" badge Official builds are only available from [GitHub Releases](https://github.com/SidhuK/Glyph/releases). **No licensing restrictions!** Builds you create yourself: * Don't include the `GLYPH_OFFICIAL_BUILD=1` flag * Skip trial and licensing entirely * Have full functionality immediately * Remain unrestricted forever This is part of Glyph's open source commitment. **Yes!** Glyph is open source. You can: * Fork the repository * Modify the code * Build custom versions * Distribute your builds (respecting the license terms) Only official binaries from the maintainers require licensing. **Never!** Your license is: * **One-time purchase** - Pay once * **Lifetime access** - No expiration * **No subscriptions** - No recurring fees * **All updates included** - Future versions covered ## Support Questions For licensing support: 1. Check [Troubleshooting](/licensing/troubleshooting) for common issues 2. Open a support ticket at [GitHub Issues](https://github.com/SidhuK/Glyph/issues) 3. Include: * Error message (if any) * Whether you're online/offline * Whether the key is from Gumroad Simply enter your license key: 1. When the lock screen appears, paste your key in the input field 2. Click **Activate License** 3. Glyph will verify with Gumroad and unlock immediately Your notes are safe and will be accessible after activation. Gumroad provides receipts automatically. Check: * Your purchase confirmation email * [Gumroad Library](https://gumroad.com/library) for purchase history For formal invoices, contact Gumroad support. ## Still Have Questions? If your question isn't answered here: * [Troubleshooting](/licensing/troubleshooting) - Common issues and solutions * [GitHub Issues](https://github.com/SidhuK/Glyph/issues) - Community support * [Purchase on Gumroad](https://karatsidhu.gumroad.com/l/sqxfay) - Buy a license # Licensing Overview Source: https://docs.glyphformac.com/licensing/overview Understanding Glyph's licensing model for official binaries Glyph is **open source** software, but official release binaries require a license after the 48-hour trial period. ## What is Licensed Official binaries published through the [GitHub release workflow](https://github.com/SidhuK/Glyph/releases) include: * **48-hour free trial** starting on first launch * **Gumroad license key activation** for lifetime access * **Offline use** after one successful activation * **Unlimited devices** with no seat limits ## What is Not Gated The following remain completely unrestricted: * **Source code access** on GitHub * **Local development builds** you build yourself * **Community builds** from contributors The app determines if it's an official build using the `GLYPH_OFFICIAL_BUILD=1` release-build flag. ## License Model Glyph uses an **honest-user licensing model** for official binaries, not DRM: **One-time purchase** - Pay once, use forever **Lifetime access** - No subscriptions or recurring fees **Unlimited devices** - Install on as many devices as you need **No device binding** - Move between machines freely **Offline forever** - No periodic online rechecks after activation ## How It Works 1. Download an official binary from [GitHub Releases](https://github.com/SidhuK/Glyph/releases) 2. Launch Glyph to start your **48-hour trial** 3. Purchase a license key from [Gumroad](https://karatsidhu.gumroad.com/l/sqxfay) 4. Enter your key in Glyph to activate 5. Glyph verifies once with Gumroad and stores a local activation record 6. Continue using Glyph offline forever ## Where Licensing Appears There are three entry points for managing your license: ### 1. Trial Banner During the 48-hour trial, Glyph shows a banner at the top of the app with: * Remaining trial time * **Enter License Key** button * **Buy on Gumroad** button ### 2. Lock Screen After the trial expires, Glyph shows a full-screen activation view before the app shell loads. You must enter a valid license key to continue. ### 3. Settings Open **Settings → General → License** or use the command palette: * `Manage license` * `Buy Glyph license` ## Purchase Ready to unlock Glyph? [Buy Glyph on Gumroad](https://karatsidhu.gumroad.com/l/sqxfay) ## Support For licensing support, visit: [GitHub Issues](https://github.com/SidhuK/Glyph/issues) # Trial System Source: https://docs.glyphformac.com/licensing/trial Understanding Glyph's 48-hour free trial Official Glyph binaries include a **48-hour free trial** that starts automatically on first launch. ## How the Trial Works When you launch an official Glyph binary for the first time: Glyph detects it's the first launch and starts a 48-hour timer. You have complete access to all features during the trial period. A banner at the top shows your remaining trial time with options to enter a license key or purchase. When the trial ends, Glyph shows a lock screen requiring license activation. ## Trial Duration The trial is exactly **48 hours** (2 days) from the moment you first launch an official build. * **Start time**: Recorded on first launch * **Expiry time**: Exactly 48 hours later * **Clock**: Based on your system time Changing your system clock will not extend the trial. Trial timestamps are recorded and compared against current time. ## Trial Banner During the trial, you'll see a banner at the top of the app: ``` 🟡 Trial | Your Glyph trial is active | [Remaining time] | [Enter License Key] [Buy on Gumroad] ``` The banner displays: * **Trial badge** indicating you're in trial mode * **Remaining time** in days, hours, and minutes * **Enter License Key** button to activate immediately * **Buy on Gumroad** button to purchase a license ## What Happens When Trial Expires After 48 hours, the trial expires and: 1. **Lock screen appears** - Glyph shows a full-screen activation view 2. **App shell is blocked** - You cannot access notes or features 3. **Settings remain accessible** - You can enter a license key to activate 4. **Purchase option available** - Direct link to buy on Gumroad ### Lock Screen The lock screen displays: ``` Official Release Glyph requires a license key This official build includes a 48-hour free trial. After that, a lifetime Gumroad license unlocks Glyph forever on all of your devices. 🔴 Trial Ended | Trial expired [time ago] [License Key input field] [Activate License] [Buy on Gumroad] [Retry Status Check] [Get Support] ``` ## Activating During Trial You don't have to wait for the trial to expire. You can activate anytime during the trial: 1. Click **Enter License Key** in the trial banner 2. Or go to **Settings → General → License** 3. Enter your license key from Gumroad 4. Click **Activate** Activating during the trial immediately removes the trial banner and unlocks Glyph permanently. ## Trial State Storage Trial information is stored locally in: ``` app_config_dir()/license.json ``` The file contains: ```json theme={null} { "version": 1, "trial_started_at": "2026-03-01T08:00:00Z", "trial_expires_at": "2026-03-03T08:00:00Z", "licensed": false } ``` Deleting or modifying this file may reset your trial state, but you won't get an additional 48 hours. The trial period is tied to the original start time. ## Community Builds vs Official Builds Only **official binaries** from GitHub Releases enforce the trial: | Build Type | Trial | License Required | | ----------------- | -------------- | ---------------- | | Official Release | Yes (48 hours) | Yes, after trial | | Community Build | No | No | | Development Build | No | No | | Self-Built | No | No | Official builds are identified by the `GLYPH_OFFICIAL_BUILD=1` flag set during the release build process. ## Checking Trial Status You can check your trial status anytime: 1. Open **Settings → General → License** 2. Look for the status badge: * 🟡 **Trial Active** - Trial is running * 🔴 **Trial Expired** - Trial has ended * 🟢 **Licensed** - Activated with license key * 🔵 **Community Build** - No licensing enforced ## Trial + Offline Use ### First Launch Offline If you first launch Glyph while offline: * Trial starts normally based on system time * No internet connection required to start trial * Trial countdown continues based on local time ### Trial Expiry Offline If your trial expires while offline: * Lock screen appears as normal * You must connect to internet to activate with Gumroad * Once activated, you can use Glyph offline forever ## Reinstalling Glyph If you reinstall Glyph or move to a new device: * **First install**: New 48-hour trial starts * **With license key**: Activate immediately to skip trial * **Multiple devices**: Each device gets its own trial, but one license key works everywhere Your license key has no device limits. Purchase once and activate on unlimited devices. ## Next Steps * [License Activation](/licensing/activation) - How to activate your license * [Purchase Glyph](https://karatsidhu.gumroad.com/l/sqxfay) - Buy a lifetime license * [FAQ](/licensing/faq) - Common questions about trials and licensing # Troubleshooting Source: https://docs.glyphformac.com/licensing/troubleshooting Common licensing issues and solutions Solutions to common problems with trial periods, license activation, and verification. ## Activation Issues **Possible causes:** * Typo in the license key * Copying extra spaces or characters * Using a key from a different product * Internet connection problems preventing Gumroad verification **Solutions:** 1. **Copy the key again** from your Gumroad purchase email 2. **Paste directly** - don't type it manually 3. **Check for spaces** - remove any leading/trailing spaces 4. **Verify internet connection** - activation requires online access 5. **Try the key on Gumroad** - verify it's valid at [gumroad.com/library](https://gumroad.com/library) **This usually means a network issue.** **Solutions:** 1. **Check internet connection** - can you browse the web? 2. **Disable VPN temporarily** - some VPNs block Gumroad API 3. **Check firewall settings** - ensure Glyph can access `api.gumroad.com` 4. **Try a different network** - mobile hotspot, different WiFi, etc. 5. **Wait and retry** - Gumroad API might be temporarily down **Network requirements:** Glyph needs to reach: ``` POST https://api.gumroad.com/v2/licenses/verify ``` **Possible causes:** * Empty license key field * Already submitting (button disabled during verification) * JavaScript error in the app **Solutions:** 1. **Ensure key is entered** - paste your license key in the input field 2. **Wait for current attempt** - if "Verifying..." is shown, wait 3. **Restart Glyph** - close and reopen the app 4. **Check for app updates** - download the latest version from GitHub Releases 5. **Open browser console** - check for JavaScript errors (Cmd/Ctrl+Shift+I) **This shouldn't happen, but if it does:** **Solutions:** 1. **Restart Glyph** - close completely and reopen 2. **Check Settings** - go to Settings → General → License to verify status 3. **Look for activation timestamp** - if present, you're licensed 4. **Hard refresh the UI** - Cmd/Ctrl+R if in dev mode If the problem persists after restart, report it on [GitHub Issues](https://github.com/SidhuK/Glyph/issues). ## Trial Issues **Possible causes:** * Clock or time zone issues * Leftover license file from previous installation * System time was changed **Solutions:** 1. **Check system time** - ensure your computer's clock is accurate 2. **Check time zone** - verify it matches your location 3. **Fresh install:** * Quit Glyph completely * Delete `app_config_dir()/license.json` * Restart Glyph for a fresh trial 4. **Or skip trial** - enter your license key immediately **This happens when:** * System clock was changed after trial started * Computer was asleep near trial expiration **Solutions:** 1. **Restart Glyph** - should show lock screen if expired 2. **Check system time** - ensure clock is accurate 3. **Enter license key** - activate to bypass trial **Actually, you can!** The trial starts based on local system time and doesn't require internet. If you're seeing a lock screen immediately: 1. Your trial might have already been used on this device 2. Check `app_config_dir()/license.json` for existing trial timestamps 3. Delete the file for a fresh trial (if legitimate) 4. Or enter your license key to activate ## License File Issues License data is stored in: ``` app_config_dir()/license.json ``` **Platform-specific paths:** * **macOS**: `~/Library/Application Support/com.glyph.app/license.json` * **Windows**: `%APPDATA%\com.glyph.app\license.json` * **Linux**: `~/.config/com.glyph.app/license.json` Don't manually edit this file unless troubleshooting. Corruption may require reactivation. **Symptoms:** * Can't activate even with valid key * Trial state is weird (negative time, wrong dates) * Lock screen appears incorrectly **Solutions:** 1. **Delete the license file**: * Quit Glyph * Navigate to `app_config_dir()/license.json` * Delete the file * Restart Glyph 2. **Reactivate** - enter your license key again 3. **Fresh trial** - if you deleted the file before activating, you get a new 48-hour trial **For developers/testers:** 1. Quit Glyph 2. Delete `app_config_dir()/license.json` 3. Restart Glyph 4. New trial starts **Note:** This only works with official builds. Development builds skip licensing entirely. Building from source with `GLYPH_OFFICIAL_BUILD` unset gives you unlimited unlicensed access for development. ## Gumroad Integration Issues **Symptoms:** * Activation fails with network errors * "Failed to verify license key" message * Works on other networks **Solutions:** 1. **Check Gumroad status** - visit [gumroad.com](https://gumroad.com) to see if it's accessible 2. **Wait and retry** - API might be temporarily unavailable 3. **Try different network** - mobile hotspot, different location 4. **Contact support** - if Gumroad is down for extended period, report to GitHub Issues If you're already activated, Gumroad being down won't affect your usage. The app works offline after initial activation. **Symptoms:** * Activation fails on corporate/school networks * Works on mobile hotspot * Other apps can access internet **Solutions:** 1. **Whitelist Gumroad domains**: ``` api.gumroad.com gumroad.com ``` 2. **Try different network** - activate on home/mobile network 3. **Contact IT** - ask them to allow Gumroad API access 4. **Activate elsewhere** - use a different network just for activation, then use offline ## Multiple Devices **The same key should work everywhere.** **If it's failing:** 1. **Check internet connection** - second device needs online access for first activation 2. **Copy key carefully** - ensure no extra characters or spaces 3. **Verify it's the same product** - confirm purchase is for Glyph 4. **Check Gumroad purchase** - log into [gumroad.com/library](https://gumroad.com/library) to confirm 5. **Contact support** - if key works on one device but not another, report it **There is NO device limit!** You can use your license on: * Unlimited computers * All platforms (Mac, Windows, Linux) * Simultaneously If you're seeing errors about device limits, this is a bug. Report it on [GitHub Issues](https://github.com/SidhuK/Glyph/issues). ## Community Build vs Official Build **This means `GLYPH_OFFICIAL_BUILD=1` was set during build.** **Solutions:** 1. **Don't set the official build flag** - it's only for release builds 2. **Rebuild without the flag**: ```bash theme={null} # Don't do this for dev builds: # GLYPH_OFFICIAL_BUILD=1 pnpm tauri build # Do this instead: pnpm tauri dev # for development pnpm tauri build # for personal builds ``` 3. **Check CI configuration** - ensure you're not using the release workflow **Check in the app:** 1. Open **Settings → General → License** 2. Look at the build indicator: * **Official Release** - requires license * **Community Build** - no licensing **Community builds:** * Don't show trial banner * Don't show lock screen * Don't have license settings ## Getting Help **Report the issue:** 1. Go to [GitHub Issues](https://github.com/SidhuK/Glyph/issues) 2. Open a new issue with: * **Error message** (exact text) * **Steps you tried** from this troubleshooting guide * **Platform** (Mac/Windows/Linux) * **Glyph version** (from Settings → About) * **Network environment** (home/corporate/VPN/etc.) 3. **Don't include your license key** in the issue! The community and maintainers will help you resolve it. **Check these places:** 1. **Email** - search for "Gumroad" in your inbox 2. **Gumroad Library** - log in at [gumroad.com/library](https://gumroad.com/library) 3. **Gumroad account** - check your purchase history **Still can't find it?** Contact **Gumroad support** (not Glyph support) - they handle purchase records and can resend your key. ## Emergency: Can't Access Notes If the trial expired and you can't activate due to technical issues, your notes are safe but temporarily inaccessible. **Options:** 1. **Troubleshoot activation** - try solutions above to unlock with your license key 2. **Find a working network** - activate on mobile hotspot or different WiFi 3. **Build from source** - clone the repo and build without the official flag 4. **Export notes manually**: * Your notes are in your workspace folder (likely `~/Documents/Glyph` or similar) * They're plain markdown files - accessible with any text editor * Copy them to safety while troubleshooting Glyph stores notes as plain markdown files in your chosen workspace directory. They're always accessible outside the app if needed. ## Still Need Help? * [FAQ](/licensing/faq) - Common questions about licensing * [GitHub Issues](https://github.com/SidhuK/Glyph/issues) - Technical support * [Gumroad Support](https://gumroad.com/help) - Purchase and key issues # Quickstart Source: https://docs.glyphformac.com/quickstart Get up and running with Glyph in 5 minutes. Create your first space, write a note, and explore key features. # Quickstart Guide This guide will help you get started with Glyph in just a few minutes. You'll create your first space, write a note, and explore some of Glyph's most useful features. **Prerequisites:** Make sure you've [installed Glyph](/installation) before starting this guide. ## Getting Started Open Glyph for the first time. You'll see the welcome screen with an animated interface. The welcome screen shows three key concepts: * **Open folder** - Choose your local space folder * **Browse files** - Open notes from your folders * **Find fast** - Use search and tags to navigate A **space** is a folder on your computer where Glyph stores your notes. Choose one of these options: * **Create New Space** - Creates a new folder for your notes * **Open Existing Folder** - Use an existing folder with markdown files * **Continue Last Space** - Return to your most recent workspace Glyph stores notes as plain markdown files. You can open any folder containing `.md` files. When you open a space, Glyph: 1. Scans for markdown files 2. Builds a search index in `.glyph/index.db` 3. Watches for file changes automatically Once your space is open, create a new note: **Using the File Tree:** * Right-click in the file tree sidebar * Select "New File" * Name your file (e.g., `welcome.md`) **Using the Command Palette:** * Press `Cmd+K` (macOS) or `Ctrl+K` (Windows/Linux) * Type "new file" * Press Enter The command palette is your fastest way to navigate Glyph. Press `Cmd+K` / `Ctrl+K` anytime to access all commands. Glyph uses a powerful markdown editor. Try these features: ### Basic Formatting ```markdown theme={null} # Heading 1 ## Heading 2 **Bold text** and *italic text* - Bullet list - Another item 1. Numbered list 2. Second item [Link text](https://example.com) ``` ### Slash Commands Type `/` in the editor to open the slash command menu: * `/h1`, `/h2`, `/h3` - Insert headings * `/code` - Code block * `/task` - Task list * `/bullet` - Bullet list ### Task Lists Create tasks with checkbox syntax: ```markdown theme={null} - [ ] Incomplete task - [x] Completed task - [ ] Another task to do ``` All tasks are indexed and appear in the Tasks view (`Cmd+Shift+T`). ## Explore Key Features ### Daily Notes Quickly create a note for today: * Press `Cmd+D` (macOS) or `Ctrl+D` (Windows/Linux) * Or use Command Palette → "Open daily note" Glyph creates a new note named with today's date (e.g., `2026-03-03.md`). By default, daily notes are created in your space root. To organize them: 1. Open Settings (`Cmd+,` or `Ctrl+,`) 2. Go to **Daily Notes** tab 3. Click **Browse** to select a folder within your space 4. All future daily notes will be created there ```typescript theme={null} // Daily note naming format (from src/lib/dailyNotes.ts:6) function formatDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; // e.g., "2026-03-03" } ``` ### Wikilinks Connect your notes with wikilinks: ```markdown Basic Wikilink theme={null} I'm writing about [[My Project]] and [[Ideas]]. ``` ```markdown With Display Text theme={null} Read more in [[project-notes|my project notes]]. ``` * Type `[[` to trigger autocomplete * Glyph will suggest existing notes * Click a wikilink to navigate to that note * Backlinks are automatically indexed Wikilinks work even if the target file doesn't exist yet. Glyph will offer to create it when you click. ### Search Everything Glyph provides fast hybrid search across all your notes: * Press `Cmd+P` (macOS) or `Ctrl+P` (Windows/Linux) * Or click the search icon in the toolbar Search supports: * **Full-text search** - Find any content * **Tag search** - Use `#tag` syntax * **Title search** - Match note names * **Fuzzy matching** - Works with typos The search index is built using SQLite and updates automatically when files change. ### AI Chat (Optional) If you want to chat with AI models: 1. Open Settings (`Cmd+,` or `Ctrl+,`) 2. Go to **AI** tab 3. Choose your provider: * **OpenAI** - Requires API key * **Anthropic** - Claude models * **Gemini** - Google AI * **Ollama** - Local models (free, no API key needed) 4. Enter your API key (or configure Ollama endpoint) 5. Select a model * Click the AI icon in the sidebar * Or press `Cmd+Shift+A` (macOS) / `Ctrl+Shift+A` (Windows/Linux) Type your message and press Enter. You can: * Ask questions about your notes * Attach files as context using `@filename` * Save AI responses as new notes * View chat history Use **Ollama** for completely local, private AI chat. No API keys or internet required. ## Keyboard Shortcuts Master these shortcuts to work faster: | Action | macOS | Windows/Linux | | --------------- | ------------- | -------------- | | Command Palette | `Cmd+K` | `Ctrl+K` | | Search | `Cmd+P` | `Ctrl+P` | | Daily Note | `Cmd+D` | `Ctrl+D` | | New File | `Cmd+N` | `Ctrl+N` | | Settings | `Cmd+,` | `Ctrl+,` | | Tasks View | `Cmd+Shift+T` | `Ctrl+Shift+T` | | AI Panel | `Cmd+Shift+A` | `Ctrl+Shift+A` | | Save | `Cmd+S` | `Ctrl+S` | Press `Cmd+/` or `Ctrl+/` to see all available keyboard shortcuts. ## Understanding Your Space When you open a space, Glyph creates a `.glyph/` folder inside it: ``` your-space/ ├── .glyph/ │ ├── index.db # Search index (SQLite) │ ├── settings.json # Space settings │ └── ... # Other metadata ├── 2026-03-03.md # Your notes ├── ideas.md └── projects/ └── project-a.md ``` Don't delete the `.glyph/` folder. It contains your search index and settings. If you do delete it, Glyph will rebuild the index automatically. ## What's Next? Learn how to use task lists, tags, and folders to organize your notes Explore AI chat modes, profiles, and context management Customize themes, fonts, and appearance in Settings Master search syntax, filters, and the link graph *** Ask questions or report issues on GitHub # Keyboard Shortcuts Source: https://docs.glyphformac.com/workspace/keyboard-shortcuts Complete reference of keyboard shortcuts in Glyph Glyph is designed for keyboard-first workflows. All major actions have keyboard shortcuts that work across macOS, Windows, and Linux. ## Platform Conventions Shortcuts adapt to your operating system: * **macOS**: Uses `Cmd` (⌘) as the primary modifier * **Windows/Linux**: Uses `Ctrl` as the primary modifier In this guide: * `Cmd/Ctrl` means `Cmd` on macOS, `Ctrl` on Windows/Linux * Modifiers are shown in this order: `Cmd/Ctrl` + `Alt` + `Shift` + `Key` Press `?` anywhere in the app to see the keyboard shortcuts help dialog. ## All Shortcuts Here's the complete reference of keyboard shortcuts in Glyph:
Action macOS Windows/Linux Context
Navigation
Toggle Sidebar `Cmd+B` `Ctrl+B` Always
Toggle Sidebar (Alt 1) `Cmd+Shift+S` `Ctrl+Shift+S` Always
Toggle Sidebar (Alt 2) `Cmd+\` `Ctrl+\` Always
Toggle AI Panel `Cmd+Shift+A` `Ctrl+Shift+A` Space open
File Operations
New Note `Cmd+N` `Ctrl+N` Space open
Open Daily Note `Cmd+Shift+D` `Ctrl+Shift+D` Space open
Save Note `Cmd+S` `Ctrl+S` Editor focused
Close Preview/Tab `Cmd+W` `Ctrl+W` Space open
Search & Command Palette
Command Palette `Cmd+K` `Ctrl+K` Always
Command Palette (Alt) `Cmd+Shift+P` `Ctrl+Shift+P` Always
Search `Cmd+F` `Ctrl+F` Always
Quick Open `Cmd+P` `Ctrl+P` Space open
Window & App
Settings `Cmd+,` `Ctrl+,` Always
Open Space `Cmd+O` `Ctrl+O` Always
AI Assistant
Attach Current Note `Cmd+Alt+A` `Ctrl+Alt+A` Note open
Attach All Open Notes `Cmd+Alt+Shift+A` `Ctrl+Alt+Shift+A` Notes open
## Shortcut Categories ### Navigation Shortcuts Control the workspace layout and panel visibility. ```typescript theme={null} // From src/lib/shortcuts/registry.ts { id: "toggle-sidebar", shortcut: { meta: true, key: "b" }, label: "Toggle Sidebar", description: "Show or hide the file tree sidebar", category: "navigation", context: "global", } ``` **Toggle Sidebar** (`Cmd+B`): Shows/hides the file tree * Has two alternative shortcuts: `Cmd+Shift+S` and `Cmd+\` * Works even when no space is open * Preserves sidebar width when re-opened **Toggle AI Panel** (`Cmd+Shift+A`): Shows/hides AI assistant * Only available when a space is open * Requires AI to be enabled in settings * Panel width is preserved between sessions ### File Operation Shortcuts Create, save, and manage notes. **New Note** (`Cmd+N`): * Creates a new markdown file in the current folder * Opens the note in the editor immediately * Prompts for a filename **Open Daily Note** (`Cmd+Shift+D`): * Creates or opens today's daily note * Requires daily notes folder to be configured * Uses format: `YYYY-MM-DD.md` **Save Note** (`Cmd+S`): * Saves the currently active editor * Auto-saves already happen, but this forces immediate save * Shows save indicator in the UI **Close Preview** (`Cmd+W`): * Closes the active preview or tab * Does not delete the file * Returns focus to file tree ### Search & Command Palette Shortcuts Quickly find files, notes, and execute commands. The command palette (`Cmd+K`) is the central hub for actions in Glyph. It has two modes: 1. **Commands Mode**: Lists all available commands 2. **Search Mode**: Full-text search across notes You can switch between modes using the tab selector or dedicated shortcuts. **Command Palette** (`Cmd+K` or `Cmd+Shift+P`): * Opens command picker * Type to filter commands * Enter to execute * Esc to close **Search** (`Cmd+F`): * Opens command palette in search mode * Searches note content, filenames, and tags * Uses hybrid search (keyword + semantic) **Quick Open** (`Cmd+P`): * File-focused search mode * Fuzzy matches filenames * Recent files appear first ### Window & App Shortcuts Manage spaces and app settings. **Settings** (`Cmd+,`): * Opens settings window * Window stays open while working * Changes apply immediately **Open Space** (`Cmd+O`): * Shows folder picker * Can create new space or open existing * Closes current space first ### AI Assistant Shortcuts Attach notes as context for AI conversations. **Attach Current Note** (`Cmd+Alt+A`): * Adds active note to AI context * Opens AI panel if closed * Appends to existing context **Attach All Open Notes** (`Cmd+Alt+Shift+A`): * Adds all open tabs to AI context * Useful for cross-referencing multiple notes * Deduplicates if note already attached ```typescript theme={null} // From src/components/app/AppShell.tsx const attachContextFiles = async (paths: string[]) => { const unique = Array.from( new Set(paths.map((p) => p.trim()).filter((p) => p.toLowerCase().endsWith(".md"))) ); if (!unique.length) return; setAiPanelOpen(true); setTimeout(() => dispatchAiContextAttach({ paths: unique }), 0); }; ``` ## Context-Sensitive Shortcuts Some shortcuts only work in specific contexts: ### Global Context Work anywhere in the app: * Command Palette (`Cmd+K`) * Settings (`Cmd+,`) * Toggle Sidebar (`Cmd+B`) * Search (`Cmd+F`) ### Space Context Require an open space: * New Note (`Cmd+N`) * Quick Open (`Cmd+P`) * Daily Note (`Cmd+Shift+D`) * Toggle AI Panel (`Cmd+Shift+A`) ### Editor Context Only when editor is focused: * Save (`Cmd+S`) * Editor-specific commands ## Customizing Shortcuts Keyboard shortcuts are currently not customizable. This feature is planned for a future release. Shortcuts are defined in the source code: ```typescript theme={null} // From src/lib/shortcuts/registry.ts export const SHORTCUTS = [ { id: "new-note", shortcut: { meta: true, key: "n" }, label: "New Note", description: "Create a new note in the current folder", category: "file", context: "space", }, // ... more shortcuts ] as const; ``` ## Shortcut Conflicts If shortcuts conflict with your OS or other apps: 1. **macOS**: System shortcuts take precedence * Disable in System Settings → Keyboard → Shortcuts 2. **Windows**: Check for conflicts with Windows shortcuts 3. **Linux**: Varies by desktop environment * `Cmd+H` (Hide on macOS) - not used by Glyph * `Cmd+M` (Minimize on macOS) - not used by Glyph * `Cmd+Tab` (App switcher) - handled by OS * `Cmd+Q` (Quit on macOS) - handled by OS * `Alt+F4` (Close on Windows) - handled by OS ## Tips for Efficiency 1. **Learn the Command Palette**: `Cmd+K` is the fastest way to any action 2. **Use Quick Open**: `Cmd+P` to jump between notes without the mouse 3. **Master sidebar toggle**: `Cmd+B` gives you more editor space 4. **Daily note ritual**: `Cmd+Shift+D` to start journaling instantly 5. **AI context workflow**: `Cmd+Alt+Shift+A` to discuss multiple notes ## Platform-Specific Key Symbols Shortcuts display with platform-native symbols: ### macOS Symbols * `⌘` Cmd (Command) * `⌃` Ctrl (Control) * `⌥` Alt (Option) * `⇧` Shift ### Windows/Linux * `Ctrl` Control * `Alt` Alt * `Shift` Shift * `Win` Windows key (rarely used) ```typescript theme={null} // From src/lib/shortcuts/platform.ts const MODIFIER_SYMBOLS = { macos: { meta: "⌘", ctrl: "⌃", alt: "⌥", shift: "⇧" }, windows: { meta: "Win", ctrl: "Ctrl", alt: "Alt", shift: "Shift" }, linux: { meta: "Super", ctrl: "Ctrl", alt: "Alt", shift: "Shift" }, }; ``` ## Viewing Shortcuts In-App Access the keyboard shortcuts help dialog: * Press `?` from anywhere * Or use Command Palette → "Keyboard Shortcuts" The dialog groups shortcuts by category and shows platform-appropriate symbols. # Settings Source: https://docs.glyphformac.com/workspace/settings Configure Glyph's appearance, AI providers, and workspace preferences Glyph's settings are organized into several categories, each accessible from the settings window (`Cmd+,` or `Ctrl+,`): * **AI Settings** - Configure AI providers and models * **Appearance** - Customize theme, colors, and fonts * **Space Settings** - Manage current workspace * **Daily Notes** - Configure daily note creation * **General** - AI assistant defaults and licensing * **About** - App version and updates ## Opening Settings Access settings in several ways: * **Keyboard**: `Cmd+,` (macOS) or `Ctrl+,` (Windows/Linux) * **Command Palette**: `Cmd+K` → "Settings" * **Menu**: Application Menu → Settings The settings window opens in a separate window, allowing you to keep it open while working. ## AI Settings Configure AI providers and models for the assistant panel. ### AI Profiles Glyph uses **profiles** to manage different AI configurations. Each profile contains: * Provider (OpenAI, Anthropic, Google, etc.) * Model selection * Base URL for custom endpoints * Custom headers for authentication * Private host access permissions * Reasoning effort (for o1-style models) 1. Navigate to **Settings → AI** 2. Click "Create Profile" to add a default profile 3. Configure these fields: * **Name**: Label for the profile (e.g., "GPT-4", "Claude Sonnet") * **Provider**: Select from OpenAI, Anthropic, Google, Groq, or Custom * **Model**: Choose the model or enter a custom model ID * **Base URL**: Optional custom API endpoint * **Headers**: Key-value pairs for authentication * **Allow Private Hosts**: Enable for localhost/private network APIs 4. Click "Save" to persist the profile The active profile is used for all AI operations in the current space. ### Supported Providers * **OpenAI**: GPT-4, GPT-4 Turbo, GPT-3.5, o1 models * **Anthropic**: Claude 3.5 Sonnet, Claude 3 Opus/Sonnet/Haiku * **Google**: Gemini 1.5 Pro, Gemini 1.5 Flash * **Groq**: Fast inference for Llama, Mixtral models * **Custom**: Any OpenAI-compatible API endpoint ### AI Availability Toggle AI features on/off globally: ```typescript theme={null} // From src/components/settings/AiSettingsPane.tsx ``` When disabled, AI panels and commands are hidden throughout the app. AI settings are stored per-profile and persist across sessions. Your API keys are stored securely in the system keychain. ## Appearance Settings Customize Glyph's visual appearance. ### Theme Mode Choose between: * **Light**: Bright background, dark text * **Dark**: Dark background, light text * **System**: Follows your OS preference The theme applies immediately without restart. ### Accent Colors Select an accent color that appears in: * Selected items * Buttons and interactive elements * Active states * Links and highlights Available accents: * Neutral (default gray) * Cerulean (blue) * Tropical Teal * Light Yellow * Soft Apricot * Vibrant Coral ```typescript theme={null} // From src/lib/settings.ts export type UiAccent = | "neutral" | "cerulean" | "tropical-teal" | "light-yellow" | "soft-apricot" | "vibrant-coral"; ``` ### Typography Configure fonts and sizes: #### Font Family Choose the primary UI and content font. Glyph loads system fonts automatically, including: * System defaults (Inter, SF Pro, Segoe UI) * Custom installed fonts #### Monospace Font Select a monospace font for: * Code blocks * Inline code * File paths * Technical content Common options: JetBrains Mono, Fira Code, Monaco, Consolas #### Font Size Adjust the base font size: * **Small**: 13px * **Medium**: 14px (default) * **Large**: 15px * **Extra Large**: 16px All UI elements scale proportionally. Glyph enumerates system fonts on startup: ```typescript theme={null} // From src/components/settings/appearanceOptions.ts export async function loadAvailableFonts(): Promise { const fonts = await invoke("system_fonts_list"); return fonts.filter((font) => !isMonospaceFont(font)); } export async function loadAvailableMonospaceFonts(): Promise { const fonts = await invoke("system_fonts_list"); return fonts.filter(isMonospaceFont); } ``` Fonts are cached per session for performance. ## Space Settings Manage the current space and its metadata. ### Current Space Displays: * Full path to the active space * Status indicator (Active/Inactive) ### Recent Spaces View and manage recently opened spaces: * List shows all recent space paths * Click "Clear" to remove all entries * Paths are stored in app settings, not in the space itself ### Search Index The search index powers fast full-text search across all notes. **Rebuild Index**: Click this button to: * Re-scan all markdown files in the space * Clear stale entries * Fix search issues * Update after bulk file operations Rebuilding is safe and non-destructive. Your notes are never modified. ### Task Sources Configure where Glyph looks for tasks (checkboxes in markdown): * Enable/disable specific folders * Set scan depth * Filter by file patterns ## Daily Notes Settings Configure automatic daily note creation. ### Daily Notes Folder Choose where daily notes are created: 1. Click "Browse" to select a folder within your space 2. Must be inside the current space (not external) 3. Use relative paths (e.g., `journal/daily`) **Shortcuts:** * `Cmd+Shift+D` (macOS) or `Ctrl+Shift+D` (Windows/Linux) creates/opens today's note ### How It Works ```typescript theme={null} // From src/components/settings/DailyNotesSettingsPane.tsx const handleBrowseFolder = async () => { const selected = await open({ directory: true, multiple: false }); if (selected && typeof selected === "string") { const currentSpacePath = await invoke("space_get_current"); // Validates selection is within space // Converts to relative path await setDailyNotesFolder(relativePath || null); } }; ``` Daily notes are named using the format: `YYYY-MM-DD.md` (e.g., `2026-03-03.md`). Currently, daily notes are created as blank files. Template support is planned for a future release. ## General Settings App-wide preferences and licensing. ### Assistant Default View Choose how the AI panel opens: * **Create View**: Shows the note creation interface * **Chat View**: Opens directly to chat mode This preference applies when toggling the AI panel with `Cmd+Shift+A`. ### License Management Manage your Glyph license: * View license status (trial, active, expired) * Enter license key * Purchase new license * Check expiration dates ```typescript theme={null} // From src/lib/license.ts export interface LicenseStatus { status: "trial" | "active" | "expired" | "invalid"; purchase_url: string; days_remaining?: number; } ``` ## Settings Storage Settings are stored in two locations: ### App Settings Stored in the system's app data folder: * Theme, accent, fonts * AI profiles and providers * Recent spaces list * License information ### Space Settings Stored in `.glyph/Glyph/` within each space: * Daily notes folder * Task sources configuration * Space-specific preferences Deleting app settings resets to defaults. Space settings travel with the space if you move or sync it. ## Keyboard Shortcuts in Settings
Action macOS Windows/Linux
Open Settings `Cmd+,` `Ctrl+,`
Close Settings `Cmd+W` `Ctrl+W`
Switch Tabs `Cmd+1-5` `Ctrl+1-5`
## Troubleshooting ### Settings not saving * Check file permissions in your home directory * Ensure you have disk space available * Try restarting Glyph ### Fonts not appearing * Font enumeration happens once at startup * Restart Glyph after installing new fonts * System fonts only (not web fonts) ### AI profile errors * Verify API keys are correct * Check base URL format for custom endpoints * Enable "Allow Private Hosts" for localhost APIs # Spaces Source: https://docs.glyphformac.com/workspace/spaces Understanding and managing your Glyph workspace A **space** in Glyph is a directory-based workspace that contains all your notes, assets, and metadata. Each space is a self-contained environment with its own search index, settings, and organization. ## What is a Space? A space is simply a folder on your computer that Glyph uses to store and organize your notes. When you create or open a space, Glyph sets up a special `.glyph/` directory inside it to store metadata, cache, and app-specific data. ### Directory Structure When you create a space, Glyph automatically creates this structure: ``` your-space/ ├── .glyph/ │ ├── glyph.sqlite # Search index database │ ├── cache/ # Cached assets and data │ └── Glyph/ │ └── ai_history/ # AI conversation history ├── notes/ # Your markdown files (optional) ├── assets/ # Images, attachments (optional) └── (your files and folders) ``` The `.glyph/` folder stores all metadata and should not be manually edited. It's automatically managed by the app. ## Creating a Space You can create a new space in two ways: 1. **From the Welcome Screen**: Click "Create Space" and choose a directory 2. **From the Menu**: Navigate to File → Create Space (or use `Cmd+Shift+N` on macOS) When you create a space, Glyph: * Creates the `.glyph/` directory structure * Initializes the search database * Sets up the file watcher for real-time updates * Cleans up any temporary files from previous sessions ```rust theme={null} // From src-tauri/src/space/helpers.rs pub fn create_or_open_impl(root: &Path) -> Result { ensure_glyph_dirs(root)?; let _ = cleanup_tmp_files(root); Ok(SpaceInfo { root: root.to_string_lossy().to_string(), schema_version: VAULT_SCHEMA_VERSION, }) } ``` ## Opening a Space To open an existing space: 1. Use `Cmd+O` (macOS) or `Ctrl+O` (Windows/Linux) 2. Select the folder containing your notes 3. Glyph will detect if it's an existing space or create a new one When you open a space, Glyph performs these operations: 1. **Validates the directory** - Ensures it's a valid folder path 2. **Initializes metadata** - Creates `.glyph/` structure if needed 3. **Starts file watcher** - Monitors changes to your files in real-time 4. **Loads the index** - Opens the SQLite database for search 5. **Cleans temporary files** - Removes `.tmp` files from crashes The file watcher (implemented in `src-tauri/src/space/watcher.rs`) monitors all changes and updates the search index automatically with a 100ms debounce. ## File Watching Glyph monitors your space directory for changes using a recursive file watcher. This means: * **Real-time updates**: Changes from external editors appear immediately * **Smart indexing**: Only modified files are re-indexed * **Change debouncing**: Multiple rapid changes are batched (100ms window) * **Hidden file filtering**: Ignores files/folders starting with `.` ### How It Works ```rust theme={null} // From src-tauri/src/space/watcher.rs const DEBOUNCE_MS: u64 = 100; // The watcher monitors three types of events: // - Create: New files or folders // - Modify: Content changes // - Remove: Deleted files or folders ``` When changes are detected: 1. External changes trigger a `space:fs_changed` event to the frontend 2. Markdown files are automatically re-indexed for search 3. The file tree UI updates to reflect changes 4. Recent local changes (within 2 seconds) are tracked to avoid duplicate processing Glyph ignores its own writes for 2 seconds to prevent re-indexing files you just saved. ## Closing a Space To close the current space: * Use File → Close Space from the menu * The file watcher stops * The search index connection closes * Recent spaces list is updated Your files remain untouched - closing a space just disconnects Glyph from monitoring it. ## Recent Spaces Glyph remembers recently opened spaces for quick access. You can: * View recent spaces in **Settings → Space** * Clear the recent list with the "Clear" button * Reopen a recent space from the welcome screen ## Space Operations Reference
Operation Command Description
Create Space `space_create` Creates directory structure and initializes a new space
Open Space `space_open` Opens existing space or creates if needed
Close Space `space_close` Stops watcher and closes index connection
Get Current `space_get_current` Returns the path of currently open space
## Best Practices 1. **Keep spaces focused**: Create separate spaces for different projects or areas 2. **Use cloud sync carefully**: If syncing with Dropbox/iCloud, exclude `.glyph/cache/` 3. **Don't nest spaces**: Avoid creating one space inside another 4. **Backup regularly**: The `.glyph/` folder can be recreated, but back up your notes Yes! You can version control your space with Git. Consider adding this to `.gitignore`: ``` .glyph/cache/ .glyph/*.tmp ``` The search database (`glyph.sqlite`) can be committed or ignored - Glyph will rebuild it if missing. ## Troubleshooting ### Space won't open * Ensure the folder exists and you have read/write permissions * Check that the path doesn't contain special characters * Try creating a new space instead ### Search not working * Go to **Settings → Space** and click "Rebuild Index" * This re-indexes all markdown files in your space ### Changes not appearing * The file watcher may have stopped - try closing and reopening the space * Check that changed files aren't in hidden folders (starting with `.`)