Skip to content
Documentation GitHub
Platform

User Settings System

Status: Shipped



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.

Settings are stored in a JSON file outside any workspace, in the OS-standard location:

PlatformPath
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.json to isolate dev/test data. See apps/desktop/src-tauri/src/paths.rs for detection logic.

Benefits:

  • Survives workspace deletion
  • Standard location for app settings
  • Human-readable JSON format
  • Atomic writes via temp file + rename

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

Settings file absence indicates first launch. On first launch:

  1. Create settings file with defaults
  2. Set first_launch_completed: false
  3. After tour completion, set to true

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

OpenRouter supports an OAuth 2.0 PKCE flow as an alternative to manual key entry. The flow:

  1. start_openrouter_auth Tauri command initiates PKCE — generates code verifier/challenge, opens OpenRouter authorization URL in default browser
  2. The inklings:// deep link URL scheme receives the OAuth callback with the authorization code
  3. complete_openrouter_auth Tauri command exchanges the code + verifier for an access token
  4. The access token is stored in the OS keychain (same as manually-entered API keys)
  5. api_key_configured is set to true and provider is set to OpenRouter in AgentSettings

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.

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.

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 commands
  • infrastructure_llm::KeyStore — async interface for provider construction

The api_key_configured boolean in AgentSettings tracks whether a key exists without exposing the secret.

{
"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
}
}
crates/domain/src/settings.rs
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 }
FieldTypeDefaultVersionDescription
schema_versionu329v1Schema version for migrations
first_launch_completedboolfalsev1Tour completed
user_personaOptionNonev1User’s primary use case
recent_workspacesVec[]v2Recently opened workspaces (max 10)
log_retention_daysu327v3Days to retain log files (1-365)
analytics_enabledbooltruev4Anonymous usage analytics (opt-out)
mcp_enabledboolfalsev5MCP server enabled (opt-in)
mcp_portu167862v5MCP server port (1024-65535)
mcp_tokenOptionNonev5Persisted MCP bearer token
event_log_retention_daysu3290v6Event log retention (7-3650)
agentOptionNonev7Agent settings sub-struct
FieldTypeDefaultDescription
providerOptionNoneSelected LLM provider
modelOptionNoneModel identifier (e.g., “claude-sonnet-4-6”)
api_key_configuredboolfalseKey exists in OS keychain
proactive_enabledboolfalsePeriodic suggestion mode
proactive_interval_minutesu3230Suggestion interval (5-120)
auto_startboolfalseStart agent on workspace open
notifications.toastbooltrueToast notifications
notifications.soundboolfalseSound notifications
capabilitiesMapPer-capability grants
ollama_urlOption<String>NoneCustom Ollama endpoint URL
openrouter_oauth_tokenOption<String>NoneOAuth PKCE access token for OpenRouter (stored in keychain)
FieldTypeDescription
namestringWorkspace display name
pathstringAbsolute path to workspace directory
last_openedstringISO 8601 timestamp
┌─────────────────────────────────────────────────────────┐
│ 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 CaseModuleDescription
GetSettingsUseCasesettings/get.rsRead current settings
IsFirstLaunchUseCasesettings/is_first_launch.rsCheck if tour is incomplete
CompleteFirstLaunchUseCasesettings/complete_first_launch.rsMark tour as completed
SetPersonaUseCasesettings/set_persona.rsSet user’s persona selection
GetAgentSettingsUseCasesettings/get_agent_settings.rsRead agent sub-settings
UpdateAgentSettingsUseCasesettings/update_agent_settings.rsReplace agent sub-settings
SetApiKeyUseCasesettings/set_api_key.rsStore key in keychain + set flag
RemoveApiKeyUseCasesettings/remove_api_key.rsRemove key from keychain + clear flag
ValidateApiKeyUseCasesettings/validate_api_key.rsValidate key against provider endpoint
AnalyticsOptOutUseCasesettings/analytics_opt_out.rsDisable analytics
AnalyticsOptInUseCasesettings/analytics_opt_in.rsEnable analytics
AnalyticsIsOptedOutUseCasesettings/analytics_is_opted_out.rsCheck analytics state

Note: Recent workspace operations are handled directly via JsonSettingsRepository methods, not through use cases.

// Core settings
get_settings(): Promise<Settings>
is_first_launch(): Promise<boolean>
complete_first_launch(): Promise<void>
set_persona(persona: UserPersona): Promise<void>
// Analytics
analytics_opt_out(): Promise<void>
analytics_opt_in(): Promise<void>
analytics_is_opted_out(): Promise<boolean>
// Agent settings
get_agent_settings(): Promise<AgentSettings | null>
update_agent_settings(agent_settings: AgentSettings): Promise<void>
// API key management
set_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 OAuth
start_openrouter_auth(): Promise<void>
complete_openrouter_auth(code: string, state: string): Promise<void>
// Recent Workspaces
list_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>
// Generated from Rust via Specta
interface 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:types after changing Rust types.

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.

VersionChanges
v1Initial JSON schema: schema_version, first_launch_completed, user_persona
v2Added recent_workspaces field (merged from separate file)
v3Added log_retention_days (1-365, default 7)
v4Added analytics_enabled (default true, opt-out)
v5Added mcp_enabled, mcp_port (7862), mcp_token
v6Added event_log_retention_days (7-3650, default 90)
v7Added agent sub-struct (provider, model, api_key_configured, capabilities, notifications)
v8Added Ollama variant to AgentProvider, ollama_url field to AgentSettings
v9Added OpenRouter variant to AgentProvider, openrouter_oauth_token field to AgentSettings
  1. Add field to Settings struct with #[serde(default)]
  2. Update Default impl if needed
  3. Bump SETTINGS_SCHEMA_VERSION (optional, for documentation)
  4. Run pnpm generate:types

No migration code needed for additive changes.

ErrorHandling
File missingReturn default settings
File emptyReturn default settings
Parse errorLog warning, return default settings
Write permission deniedReturn error to frontend
Keychain access deniedReturn error to caller
Keychain item not foundReturn None (no key configured)

Graceful degradation: Settings errors never crash the app. Invalid files are replaced with defaults.

  • 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

Settings system is required by Agent Harness, MCP Server, Analytics, and First Launch Experience.

Was this page helpful?