User Settings System
Status: Shipped
Diagram
Section titled “Diagram”Overview
Section titled “Overview”The User Settings System manages application preferences and recent workspaces stored in a JSON file in the OS-standard
app data directory. Agent API keys are stored separately in the OS keychain for security — only a boolean flag
(api_key_configured) in the settings JSON indicates whether a key exists.
Key Design Decisions
Section titled “Key Design Decisions”1. OS App Data Location
Section titled “1. OS App Data Location”Settings are stored in a JSON file outside any workspace, in the OS-standard location:
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/Inklings/settings.json |
| Windows | %APPDATA%\Inklings\settings.json |
| Linux | ~/.config/inklings/settings.json |
Dev Mode: In development builds, settings are stored in
{project}/.data/settings/settings.jsonto isolate dev/test data. Seeapps/desktop/src-tauri/src/paths.rsfor detection logic.
Benefits:
- Survives workspace deletion
- Standard location for app settings
- Human-readable JSON format
- Atomic writes via temp file + rename
2. JSON Storage with Unified State
Section titled “2. JSON Storage with Unified State”Settings and recent workspaces are stored together in a single JSON file. This consolidation:
- Reduces coordination complexity (one repository instead of two)
- Ensures atomic updates to related data
- Simplifies the codebase
3. First-Launch Detection
Section titled “3. First-Launch Detection”Settings file absence indicates first launch. On first launch:
- Create settings file with defaults
- Set
first_launch_completed: false - After tour completion, set to
true
4. UI Preferences in Frontend
Section titled “4. UI Preferences in Frontend”Most UI preferences (theme, sidebar state, font sizes) are handled by the frontend using localStorage. The backend
only stores:
- First launch state
- User persona selection
- Recent workspaces list
- Agent configuration
- MCP server settings
- Analytics and retention preferences
5. OpenRouter OAuth PKCE Flow
Section titled “5. OpenRouter OAuth PKCE Flow”OpenRouter supports an OAuth 2.0 PKCE flow as an alternative to manual key entry. The flow:
start_openrouter_authTauri command initiates PKCE — generates code verifier/challenge, opens OpenRouter authorization URL in default browser- The
inklings://deep link URL scheme receives the OAuth callback with the authorization code complete_openrouter_authTauri command exchanges the code + verifier for an access token- The access token is stored in the OS keychain (same as manually-entered API keys)
api_key_configuredis set totrueandprovideris set toOpenRouterinAgentSettings
The openrouter_oauth_token field in AgentSettings is a documentation-only placeholder; the actual token is in the OS
keychain. The boolean flag api_key_configured is the authoritative state indicator, consistent with all other
providers.
6. Agent Setup Guided Flow
Section titled “6. Agent Setup Guided Flow”When agent settings are null (never configured), the AgentSettings React component shows a three-step guided setup
flow instead of the full settings panel.
Step 0 — Choose your path: Three cards present the available configuration paths:
- Local Model (Free) — sets provider to
ollama, then loads the Ollama status + model picker in the full settings panel - Cloud via OpenRouter — sets provider to
open_router, then shows the OAuth connect flow in the full settings panel - Cloud Provider (Direct) — no initial provider set; the user picks Anthropic, OpenAI, or xAI from the provider dropdown in the full settings panel
Step 1 — Transitional: A brief “Configuring…” placeholder shown while the async handleConfigure() call completes.
Once the backend responds, notConfigured becomes false and the regular settings panel renders with the pre-selected
provider already active.
A “Skip to advanced settings” link at the bottom of Step 0 invokes handleConfigure() with no initial provider, landing
directly in the full settings panel — preserving the previous simple “Configure” button behavior.
The handleConfigure() function accepts an optional initialProvider?: AgentProvider parameter that is included in the
defaults saved to the backend.
7. API Key Security
Section titled “7. API Key Security”API keys are never stored in settings.json. They are stored in the OS keychain via KeychainKeyStore, which
implements both:
application::settings::KeychainStore— sync interface for Tauri commandsinfrastructure_llm::KeyStore— async interface for provider construction
The api_key_configured boolean in AgentSettings tracks whether a key exists without exposing the secret.
Settings Schema
Section titled “Settings Schema”{ "schema_version": 9, "first_launch_completed": true, "user_persona": "pkm", "recent_workspaces": [ { "name": "My Project", "path": "/path/to/workspace", "last_opened": "2026-01-29T12:34:56Z" } ], "log_retention_days": 7, "analytics_enabled": true, "mcp_enabled": false, "mcp_port": 7862, "mcp_token": null, "event_log_retention_days": 90, "agent": { "provider": "anthropic", "model": "claude-sonnet-4-6", "api_key_configured": true, "proactive_enabled": false, "proactive_interval_minutes": 30, "auto_start": false, "notifications": { "toast": true, "sound": false }, "capabilities": { "PagesRead": true, "SearchUse": true }, "ollama_url": "http://localhost:11434", "openrouter_oauth_token": null }}Domain Representation
Section titled “Domain Representation”pub const SETTINGS_SCHEMA_VERSION: u32 = 9;
pub struct Settings { pub schema_version: u32, pub first_launch_completed: bool, pub user_persona: Option<UserPersona>, pub recent_workspaces: Vec<RecentWorkspace>, pub log_retention_days: u32, // v3 pub analytics_enabled: bool, // v4 pub mcp_enabled: bool, // v5 pub mcp_port: u16, // v5 pub mcp_token: Option<String>, // v5 pub event_log_retention_days: u32, // v6 pub agent: Option<AgentSettings>, // v7}
pub struct AgentSettings { pub provider: Option<AgentProvider>, pub model: Option<String>, pub api_key_configured: bool, pub proactive_enabled: bool, pub proactive_interval_minutes: u32, pub auto_start: bool, pub notifications: NotificationSettings, pub capabilities: HashMap<String, bool>, pub ollama_url: Option<String>, // v8 pub openrouter_oauth_token: Option<String>, // v9}
pub struct NotificationSettings { pub toast: bool, // default: true pub sound: bool, // default: false}
pub enum UserPersona { Pkm, Writer, GameDesigner }pub enum AgentProvider { Anthropic, OpenAi, Xai, Ollama, OpenRouter }Settings Fields
Section titled “Settings Fields”| Field | Type | Default | Version | Description |
|---|---|---|---|---|
schema_version | u32 | 9 | v1 | Schema version for migrations |
first_launch_completed | bool | false | v1 | Tour completed |
user_persona | Option | None | v1 | User’s primary use case |
recent_workspaces | Vec | [] | v2 | Recently opened workspaces (max 10) |
log_retention_days | u32 | 7 | v3 | Days to retain log files (1-365) |
analytics_enabled | bool | true | v4 | Anonymous usage analytics (opt-out) |
mcp_enabled | bool | false | v5 | MCP server enabled (opt-in) |
mcp_port | u16 | 7862 | v5 | MCP server port (1024-65535) |
mcp_token | Option | None | v5 | Persisted MCP bearer token |
event_log_retention_days | u32 | 90 | v6 | Event log retention (7-3650) |
agent | Option | None | v7 | Agent settings sub-struct |
Agent Settings Fields
Section titled “Agent Settings Fields”| Field | Type | Default | Description |
|---|---|---|---|
provider | Option | None | Selected LLM provider |
model | Option | None | Model identifier (e.g., “claude-sonnet-4-6”) |
api_key_configured | bool | false | Key exists in OS keychain |
proactive_enabled | bool | false | Periodic suggestion mode |
proactive_interval_minutes | u32 | 30 | Suggestion interval (5-120) |
auto_start | bool | false | Start agent on workspace open |
notifications.toast | bool | true | Toast notifications |
notifications.sound | bool | false | Sound notifications |
capabilities | Map | Per-capability grants | |
ollama_url | Option<String> | None | Custom Ollama endpoint URL |
openrouter_oauth_token | Option<String> | None | OAuth PKCE access token for OpenRouter (stored in keychain) |
Recent Workspace Entry
Section titled “Recent Workspace Entry”| Field | Type | Description |
|---|---|---|
name | string | Workspace display name |
path | string | Absolute path to workspace directory |
last_opened | string | ISO 8601 timestamp |
Architecture
Section titled “Architecture”Repository Pattern
Section titled “Repository Pattern”┌─────────────────────────────────────────────────────────┐│ Application Layer ││ ││ SettingsRepository trait ││ - load() -> Settings ││ - save(&Settings) ││ - exists() -> bool ││ ││ KeychainStore trait ││ - set_key(provider, key) -> Result<()> ││ - get_key(provider) -> Result<Option<String>> ││ - remove_key(provider) -> Result<()> │└─────────────────────────────────────────────────────────┘ │ │ implements ▼┌─────────────────────────────────────────────────────────┐│ Infrastructure Layer ││ ││ JsonSettingsRepository ││ - Trait implementation (load/save/exists) ││ - Concrete methods for recent workspaces ││ ││ KeychainKeyStore ││ - implements KeychainStore (sync, for Tauri cmds) ││ - implements KeyStore (async, for LLM provider) ││ - OS keychain via security-framework crate │└─────────────────────────────────────────────────────────┘Why concrete methods? Recent workspace operations are implementation-specific to JSON storage. They’re not part of the trait because no other storage backend would store them differently.
Use Cases
Section titled “Use Cases”| Use Case | Module | Description |
|---|---|---|
GetSettingsUseCase | settings/get.rs | Read current settings |
IsFirstLaunchUseCase | settings/is_first_launch.rs | Check if tour is incomplete |
CompleteFirstLaunchUseCase | settings/complete_first_launch.rs | Mark tour as completed |
SetPersonaUseCase | settings/set_persona.rs | Set user’s persona selection |
GetAgentSettingsUseCase | settings/get_agent_settings.rs | Read agent sub-settings |
UpdateAgentSettingsUseCase | settings/update_agent_settings.rs | Replace agent sub-settings |
SetApiKeyUseCase | settings/set_api_key.rs | Store key in keychain + set flag |
RemoveApiKeyUseCase | settings/remove_api_key.rs | Remove key from keychain + clear flag |
ValidateApiKeyUseCase | settings/validate_api_key.rs | Validate key against provider endpoint |
AnalyticsOptOutUseCase | settings/analytics_opt_out.rs | Disable analytics |
AnalyticsOptInUseCase | settings/analytics_opt_in.rs | Enable analytics |
AnalyticsIsOptedOutUseCase | settings/analytics_is_opted_out.rs | Check analytics state |
Note: Recent workspace operations are handled directly via
JsonSettingsRepositorymethods, not through use cases.
API Surface
Section titled “API Surface”Tauri Commands
Section titled “Tauri Commands”// Core settingsget_settings(): Promise<Settings>is_first_launch(): Promise<boolean>complete_first_launch(): Promise<void>set_persona(persona: UserPersona): Promise<void>
// Analyticsanalytics_opt_out(): Promise<void>analytics_opt_in(): Promise<void>analytics_is_opted_out(): Promise<boolean>
// Agent settingsget_agent_settings(): Promise<AgentSettings | null>update_agent_settings(agent_settings: AgentSettings): Promise<void>
// API key managementset_api_key(provider: AgentProvider, key: string): Promise<void>remove_api_key(provider: AgentProvider): Promise<void>validate_api_key(provider: AgentProvider, key: string): Promise<ApiKeyValidationResult>
// OpenRouter OAuthstart_openrouter_auth(): Promise<void>complete_openrouter_auth(code: string, state: string): Promise<void>
// Recent Workspaceslist_recent_workspaces(): Promise<RecentWorkspace[]>add_to_recent_workspaces(name: string, path: string): Promise<void>remove_from_recent_workspaces(path: string): Promise<void>clear_recent_workspaces(): Promise<void>TypeScript Types
Section titled “TypeScript Types”// Generated from Rust via Spectainterface Settings { schema_version: number; first_launch_completed: boolean; user_persona: UserPersona | null; recent_workspaces: RecentWorkspace[]; log_retention_days: number; analytics_enabled: boolean; mcp_enabled: boolean; mcp_port: number; mcp_token: string | null; event_log_retention_days: number; agent: AgentSettings | null;}
type UserPersona = 'pkm' | 'writer' | 'game_designer';type AgentProvider = 'anthropic' | 'open_ai' | 'xai' | 'ollama' | 'open_router';type ApiKeyValidationResult = 'Valid' | 'Invalid' | 'RateLimited' | { NetworkError: string };
interface AgentSettings { provider: AgentProvider | null; model: string | null; api_key_configured: boolean; proactive_enabled: boolean; proactive_interval_minutes: number; auto_start: boolean; notifications: NotificationSettings; capabilities: Record<string, boolean>; ollama_url: string | null; openrouter_oauth_token: string | null;}
interface NotificationSettings { toast: boolean; sound: boolean;}Note: Types are generated from Rust via Specta. Run
pnpm generate:typesafter changing Rust types.
Schema Evolution
Section titled “Schema Evolution”Backward Compatibility
Section titled “Backward Compatibility”New fields use #[serde(default)] so old JSON files deserialize correctly:
#[serde(default = "default_analytics_enabled")]pub analytics_enabled: bool,
#[serde(default)]pub agent: Option<AgentSettings>,When an old file without these fields is loaded, they default to their specified values.
Version History
Section titled “Version History”| Version | Changes |
|---|---|
| v1 | Initial JSON schema: schema_version, first_launch_completed, user_persona |
| v2 | Added recent_workspaces field (merged from separate file) |
| v3 | Added log_retention_days (1-365, default 7) |
| v4 | Added analytics_enabled (default true, opt-out) |
| v5 | Added mcp_enabled, mcp_port (7862), mcp_token |
| v6 | Added event_log_retention_days (7-3650, default 90) |
| v7 | Added agent sub-struct (provider, model, api_key_configured, capabilities, notifications) |
| v8 | Added Ollama variant to AgentProvider, ollama_url field to AgentSettings |
| v9 | Added OpenRouter variant to AgentProvider, openrouter_oauth_token field to AgentSettings |
Adding New Fields
Section titled “Adding New Fields”- Add field to
Settingsstruct with#[serde(default)] - Update
Defaultimpl if needed - Bump
SETTINGS_SCHEMA_VERSION(optional, for documentation) - Run
pnpm generate:types
No migration code needed for additive changes.
Error Handling
Section titled “Error Handling”| Error | Handling |
|---|---|
| File missing | Return default settings |
| File empty | Return default settings |
| Parse error | Log warning, return default settings |
| Write permission denied | Return error to frontend |
| Keychain access denied | Return error to caller |
| Keychain item not found | Return None (no key configured) |
Graceful degradation: Settings errors never crash the app. Invalid files are replaced with defaults.
Testing Strategy
Section titled “Testing Strategy”- Domain: Settings validation, persona serialization, agent settings roundtrip, proactive interval clamping
- Application: Use case tests with mocked repos and keychain stores (27 tests)
- Infrastructure: JSON roundtrip, atomic writes, backward compatibility
- E2E: Settings persist across app restart, recent workspaces accumulate
Related Systems
Section titled “Related Systems”- Workspace System — Uses settings for recent workspaces
- First Launch Experience — Reads/writes first_launch_completed
- LLM System — Uses KeyStore for API key retrieval during provider construction
- Agent Harness — Reads AgentSettings for config
- MCP Server — Uses mcp_enabled, mcp_port, mcp_token
- Event Log System — Uses event_log_retention_days
Settings system is required by Agent Harness, MCP Server, Analytics, and First Launch Experience.
Was this page helpful?
Thanks for your feedback!