# 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: ''
// }
// 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 */}