Skip to main content

Stores

Reactive state that needs to be shared across components lives in resources/js/stores/. Each store is a TypeScript class using Svelte 5 Runes ($state, $derived) exported as a module-level singleton. Because JavaScript modules are singletons within a page load, every component that imports the same store reads from and writes to the same reactive instance — no prop-drilling or context setup required. Store files use the .svelte.ts extension so the Svelte compiler processes the runes.

Each store file exports both the class and a pre-constructed singleton:

// resources/js/stores/MyStore.svelte.ts
export class MyStore {
public count = $state(0);
public doubled = $derived(this.count * 2);
}

export const myStore = new MyStore();

Import the singleton in any component or utility:

<script lang="ts">
import {myStore} from '$lib/stores/MyStore.svelte.js';
</script>

<p>Count: {myStore.count}</p>

Note the .js extension in imports — Vite resolves .svelte.ts files when a .js extension is used, which is the standard TypeScript ESM convention.

All stores are populated during the bootstrap sequence and are safe to read after the main stage completes. The keychainStore is the exception — it loads asynchronously once the user's passkey becomes available (see Keychain Store below).

import { aiModelStore } from '$lib/stores/AiModelStore.svelte.js';

// Inside a component or $derived — reactive, updates automatically
const models = $derived(aiModelStore.models);

Store Overview

StoreImportWhat it holds
aiModelStore$lib/stores/AiModelStore.svelte.jsAll available AI models and system-role assignments
aiToolStore$lib/stores/AiToolStore.svelte.jsAI tools and capability definitions
systemPromptStore$lib/stores/SystemPromptStore.svelte.jsServer-configured system prompts
keychainStore$lib/stores/KeychainStore.svelte.jsUser's encryption keys (async load)
themeStore$lib/stores/ThemeStore.svelte.jsActive UI theme ('dark' / 'light')
aiHandleStore$lib/stores/AiHandleStore.svelte.jsConfigured @handle string for mention parsing

AiModelStore

Holds all AI models returned by the API. The most commonly used store — it is the source of truth for which models are available, which one is active, and which system-role assignments are configured.

import { aiModelStore } from '$lib/stores/AiModelStore.svelte.js';

Key properties

PropertyTypeDescription
modelsAiModel[]All available models in API order. Reactive.
systemModelsRecord<string, AiModel>System-role assignments keyed by type string.

AiModel shape

Notable fields on the AiModel type (from $lib/schemas/resources/ai-models.schema.js):

FieldTypeNotes
activebooleanWhether the model is enabled in the admin panel.
model_type'chat' | 'image_generation' | 'video_generation' | nullDiscriminates the union — use to narrow to ChatAiModel.
native_capabilitiesstring[] | nullCapability IDs the model supports natively (e.g. 'web_search'). Replaces the old capabilities record.
flagsstring[] | nullBadge-style flags (e.g. 'eco-friendly', 'feature-streaming'). Full list in ai-model-flags.ts.
limits{ max_input_tokens, max_output_tokens } | nullToken limits. Present only on model_type === 'chat' models.
pricing{ is_free } | { ranges, priority_ranges } | nullPricing info. Present only on model_type === 'chat' models.

Key methods

getOneById(modelId) — Accepts an AiModel object, a numeric ID, or a model_id string. Returns null when no match is found.

getModelByIdOrFallback(modelId, fallbackType?) — Like getOneById but never returns null. Falls back to the model assigned to fallbackType (default: 'default'), then to the first available model. Use this when building a chat request and a concrete model is always required.

getSystemModelByType(type) — Returns the model assigned to a system role, or null.

// Get the model currently selected (with safe fallback)
const model = aiModelStore.getModelByIdOrFallback(selectedModelId);

// Get the system default
const defaultModel = aiModelStore.getSystemModelByType('default');

AiToolStore

Holds all registered AI tools and capabilities as a single merged list. Import the singleton:

import { aiToolStore } from '$lib/stores/AiToolStore.svelte.js';

Key properties

PropertyTypeDescription
toolsAiToolOrCapability[]All registered tools and capabilities, merged into one reactive list.

AiToolOrCapability

Each entry in tools is either an AiToolWrapper (a plain tool) or an AiToolCapabilityWrapper (a capability that groups related tools). Discriminate with is_capability:

import type { AiToolOrCapability } from '$lib/stores/aiToolStoreData.js';

for (const item of aiToolStore.tools) {
if (item.is_capability) {
// AiToolCapabilityWrapper
const toolsForModel = item.getToolsFor(currentModel);
const isNative = item.hasNativeCapabilityFor(currentModel);
} else {
// AiToolWrapper (plain tool)
const available = item.isAvailableFor(currentModel);
}
}

Both types share these members:

MemberTypeNotes
is_capabilitybooleanfalse for plain tools, true for capability wrappers.
displayNamestring (getter)Localized display name.
isAvailableFor(model, withOffline?)methodtrue when the model supports this item. For capabilities: true when the model has a native capability for it or has at least one matching non-offline tool. Pass withOffline: true to count offline tools.

AiToolCapabilityWrapper additionally exposes:

MemberNotes
hasNativeCapabilityFor(model)true when model.native_capabilities includes this capability's id.
getTools()All tools grouped under this capability.
getToolsFor(model)Tools for this capability that the model supports.

SystemPromptStore

Holds the server-configured system prompts. Populated during bootstrap.

import { systemPromptStore } from '$lib/stores/SystemPromptStore.svelte.js';

getPromptByType(type) — Looks up a prompt by prompt_type. When called with a WellKnownSystemPromptType constant the return type is non-nullable, eliminating a null-check at the call site.

const chatPrompt = systemPromptStore.getPromptByType('chat');
// chatPrompt is SystemPrompt (non-nullable for known type strings)

KeychainStore

Exposes the user's end-to-end encryption keys as reactive $state properties. Loading is deferred until the user's passkey becomes available on the legacy bridge — waitingToLoad resolves when the initial load is complete.

import { keychainStore } from '$lib/stores/KeychainStore.svelte.js';
PropertyTypeDescription
publicKeyCryptoKey | nullRSA public key. null until loaded.
privateKeyCryptoKey | nullRSA private key. null until loaded.
aiConvKeyCryptoKey | nullShared AES key for AI conversations. null until loaded.
roomKeysRecord<string, RoomKeys>Per-room keys keyed by slug. Empty until loaded.
waitingToLoadPromise<void>Resolves once the initial key load completes.

For cryptographic operations involving these keys, see the Encryption section in Advanced.


ThemeStore

Tracks and controls the active UI theme. Reading theme inside a $derived or component template is reactive — the component re-renders when the theme changes.

import { themeStore } from '$lib/stores/ThemeStore.svelte.js';

// Read
const isDark = $derived(themeStore.theme === 'dark');

// Write (updates <html> class list + reactive value in one step)
themeStore.theme = 'light';

The store observes the <html> class list via a MutationObserver, so it stays in sync even when the theme is toggled by legacy code outside the Svelte layer.


AiHandleStore

Provides the configured @handle string and a parser for detecting handle mentions in chat messages.

import { aiHandleStore } from '$lib/stores/AiHandleStore.svelte.js';

// The configured handle string (e.g. '@hawki')
const handle = aiHandleStore.hawkiHandle;

// Detect mentions in a message
for (const found of aiHandleStore.getHandlesIn(messageText)) {
// found === '@hawki' (only currently known handle)
}

getHandlesIn(message) is a generator. It yields each recognized handle found in the string. Currently only the single configured HAWKI handle is matched, but the method is structured to support additional handles once assistant personas are introduced.