Architecture

Data model (Dexie v7 schema)

All persistent data lives in IndexedDB via Dexie 4.4. The schema is declared in src/utils/db.ts:

StoreIndexesShape
workspacesid, type, parentId{ id, name, avatar, type: 'workspace'|'folder', parentId, vars, indexContent, defaultAssistantId?, lastDialogId?, listOpen: {assistants,artifacts,dialogs} }
dialogsid, workspaceId{ id, name, workspaceId, assistantId?, msgTree, msgRoute, inputVars, modelOverride? }
messagesid, type, dialogId{ id, type: 'user'|'assistant', assistantId?, dialogId, contents, status, generatingSession?, error?, warnings?, usage?, modelName? }
assistantsid, workspaceId{ id, name, avatar, prompt, promptVars, promptTemplate, provider, model?, modelSettings, workspaceId, plugins, promptRole, contextNum?, stream, description?, author?, homepage? }
artifactsid, workspaceId{ id, name, workspaceId, versions: ArtifactVersion[], currIndex, readable, writable, open }
installedPluginsV2key, idPlugin install records (lobechat / gradio / mcp)
reactiveskeyUI reactive state per key (settings, current selection, etc.)
avatarImagesidCached generated avatars (Material color palette)
imageCacheurlFetched image cache
itemsid, type, dialogIdTool-call result items (images, files, search hits) referenced from messages
providersidUser-defined custom providers (mix-and-match subproviders)

The canvases store is in the schema but deprecated — it was the old canvas API, replaced by artifacts. Migration preserves old data but new writes go to artifacts.

Branching chat — the tree structure

Each dialog stores messages in a tree, not a list. The two key fields on the dialog are:

  • msgTree: Record<string, string[]> — maps each message id to its child message ids. Always starts with $root.
  • msgRoute: number[] — the currently displayed path, as indices into each level's children array.

When you click "edit" on a past message, the assistant message that follows is duplicated, the new branch is added under the edited user message, and msgRoute shifts to point at the new index. The tree can branch at every user message; older branches remain accessible via the chat router UI.

This logic lives in src/composables/use-dialog-chain.ts (functions getChain, switchChain, deleteBranch, appendMessage).

Routing

Defined in src/router/routes.ts:

RoutePageNotes
/workspaces/:wid/WorkspacePage + drawerWorkspace home (left drawer with assistants / dialogs)
/workspaces/:wid/settingsWorkspaceSettingsDefault assistant, home content, vars
/workspaces/:wid/dialogs/:didDialogViewChat with branching tree
/workspaces/:wid/assistants/:aidAssistantViewEdit assistant config
/workspaces/:wid/assistants/:aid/plugins/:pidPluginAdjustPer-assistant plugin config
/settings/SettingsViewProviders, features, shortcuts, theme
/settings/shortcut-keysShortcutKeysKey binding editor
/settings/providers/:idCustomProviderEdit custom provider
/plugins/PluginsMarketInstalled + market plugins
/plugins/:pidPluginSettingsPer-plugin settings
/assistants/AssistantsMarketBrowse / import assistants
/assistants/:aidAssistantView (same editor)
/set-providerSetProviderOnboarding wizard
/accountAccountPageOnly if DexieDBURL is set (fork hides this)
/model-pricingModelPricingOnly if both DexieDBURL and LitellmBaseURL are set

All routes share MainLayout. Catch-all 404 lands on ErrorNotFound.

Pinia stores

In src/stores/:

  • workspaces.ts — CRUD for workspaces, folders, dialogs (cascading delete)
  • assistants.ts — CRUD for assistants
  • providers.ts — built-in + custom provider factory; supports subprovider fallback
  • plugins.ts — 8 builtin + 6 always-on + 3 installed kinds (lobechat / gradio / mcp); MCP uses @modelcontextprotocol/sdk with stdio/http/sse transports
  • user-data.ts — export / import all IndexedDB stores to/from JSON
  • user-perfs.ts — 38 user preferences persisted via persistentReactive
  • ui-state.ts — transient UI state (drawers, modals)

i18n

Three locales: en-US, zh-CN, zh-TW. Source under src/i18n/<locale>/ as TypeScript modules. Auto-detection: navigator.language → matching locale, fallback to en-US. Quasar's own language pack switches to match. The selection is persisted in localData (browser localStorage).

Adding a language requires creating a new folder under src/i18n/ and adding its name to the languages set in src/boot/i18n.ts.

Runtime platform detection

src/utils/platform-api.ts exports IsTauri, IsCapacitor, IsWeb, CapacitorPlatform, TauriPlatform. All platform-specific code paths branch on these:

  • fetch — uses Tauri HTTP plugin on desktop, Capacitor stream-fetch on Android, native fetch on iOS/web
  • clipboardReadText — Tauri clipboard plugin / Capacitor Clipboard / web Clipboard API
  • exportFile — direct write to Documents/AiaW/ on Android, Quasar's exportFile on web