Files
Memoh/apps/web/AGENTS.md
T
2026-04-14 21:56:42 +08:00

488 lines
28 KiB
Markdown

# Web Frontend (apps/web)
## Overview
`@memohai/web` is the management UI for Memoh, built with Vue 3 + Vite. It provides a chat interface for interacting with bots, plus visual configuration for bots, models, channels, memory, and more.
## Tech Stack
| Category | Technology |
|----------|-----------|
| Framework | Vue 3 (Composition API, `<script setup>`) |
| Build | Vite 8 + `@vitejs/plugin-vue` |
| CSS | Tailwind CSS 4 (CSS-based config, no `tailwind.config.*`) |
| UI Library | `@memohai/ui` (built on Reka UI + class-variance-authority) |
| State | Pinia 3 + `pinia-plugin-persistedstate` |
| Data Fetching | Pinia Colada (`@pinia/colada`) + `@memohai/sdk` |
| Forms | vee-validate + `@vee-validate/zod` + Zod |
| i18n | vue-i18n (en / zh) |
| Icons | lucide-vue-next (primary) + `@memohai/icon` (brand/provider icons) |
| Toast | vue-sonner |
| Tables | @tanstack/vue-table |
| Markdown | markstream-vue + Shiki + Mermaid + KaTeX |
| Charts | ECharts + vue-echarts |
| Terminal | @xterm/xterm + @xterm/addon-fit + @xterm/addon-serialize |
| Code Editor | Monaco Editor + stream-monaco |
| Utilities | @vueuse/core, @vueuse/integrations |
| Animation | animate.css + tw-animate-css |
| TypeScript | ~5.9 (strict mode) |
## Directory Structure
```
src/
├── App.vue # Root component (RouterView + Toaster + settings init)
├── main.ts # App entry (plugins, global components, API client setup)
├── router.ts # Route definitions, auth guard, chunk error recovery
├── style.css # Tailwind imports (delegates to @memohai/ui/style.css)
├── i18n.ts # vue-i18n configuration
├── assets/ # Static assets (logo.png)
├── components/ # Shared components
│ ├── sidebar/ # Bot list sidebar (collapsible, bot items, settings link)
│ │ ├── index.vue # Sidebar with bot list + settings gear footer
│ │ └── bot-item.vue # Individual bot entry in sidebar
│ ├── settings-sidebar/ # Settings section sidebar (back-to-chat + nav items)
│ ├── main-container/ # Main content area (KeepAlive RouterView)
│ ├── master-detail-sidebar-layout/ # Master-detail layout pattern
│ ├── chat-list/ # Chat list helpers
│ │ └── channel-badge/ # Channel badge component
│ ├── chat/ # Chat UI sub-components
│ │ ├── chat-status/ # Chat connection status indicator
│ │ └── chat-step/ # Chat processing step indicator
│ ├── file-manager/ # File browser (list + viewer + utils)
│ ├── terminal/ # Terminal emulator wrapper (xterm)
│ ├── monaco-editor/ # Monaco code editor wrapper
│ ├── model-capabilities/ # Model capabilities display
│ ├── context-window-badge/ # Context window size badge
│ ├── bot-select/ # Bot selection dropdown
│ ├── form-dialog-shell/ # Dialog wrapper for forms
│ ├── confirm-popover/ # Confirmation popover
│ ├── loading-button/ # Button with loading state
│ ├── status-dot/ # Status indicator dot
│ ├── channel-icon/ # Channel platform icon
│ ├── provider-icon/ # LLM provider icon (icons.ts + index.vue)
│ ├── search-provider-logo/ # Search provider icons (custom-icons.ts + index.vue)
│ ├── searchable-select-popover/ # Searchable dropdown
│ ├── timezone-select/ # Timezone selector
│ ├── key-value-editor/ # Key-value pair editor
│ ├── import-models-dialog/ # Bulk model import dialog
│ ├── add-platform/ # Add platform dialog
│ ├── add-provider/ # Add LLM provider dialog
│ └── create-model/ # Create model dialog
├── composables/ # Reusable composition functions
│ ├── api/ # API-related composables
│ │ ├── useChat.ts # Aggregated re-export of chat composables
│ │ ├── useChat.types.ts # Bot, Session, Message, StreamEvent types
│ │ ├── useChat.chat-api.ts # Bot/session CRUD (fetchBots, fetchSessions, etc.)
│ │ ├── useChat.message-api.ts # Message fetch, SSE streaming, local channel
│ │ ├── useChat.sse.ts # SSE stream reader and parser
│ │ ├── useChat.sse.test.ts # SSE parser tests
│ │ ├── useChat.ws.ts # WebSocket connection (send, abort, reconnect)
│ │ ├── useChat.ws.test.ts # WebSocket tests
│ │ ├── useChat.content.ts # Message content parsing (tool calls, text, reasoning)
│ │ ├── useContainerStream.ts # Container creation SSE stream
│ │ └── usePlatform.ts # Platform list query + create mutation
│ ├── useDialogMutation.ts # Mutation wrapper with toast error handling
│ ├── useRetryingStream.ts # SSE retry with exponential backoff
│ ├── useSyncedQueryParam.ts # URL query param sync
│ ├── useBotStatusMeta.ts # Bot status metadata
│ ├── useAvatarInitials.ts # Avatar initial generation
│ ├── useClipboard.ts # Clipboard utilities
│ ├── useKeyValueTags.ts # Tag management
│ ├── usePinnedBots.ts # Pinned bots management
│ ├── useShikiHighlighter.ts # Shiki syntax highlighter singleton
│ └── useTerminalCache.ts # Terminal output cache
├── constants/ # Constants
│ ├── client-types.ts # LLM client type definitions
│ ├── compatibilities.ts # Feature compatibility flags
│ └── acl-presets.ts # ACL preset configurations
├── i18n/locales/ # Translation files (en.json, zh.json)
├── layout/
│ └── main-layout/ # Top-level layout (SidebarProvider)
├── lib/
│ └── api-client.ts # SDK client setup (base URL, auth interceptor)
├── pages/ # Route page components
│ ├── login/ # Login page
│ ├── main-section/ # Chat section layout (bot sidebar + main container)
│ ├── settings-section/ # Settings section layout (settings sidebar + KeepAlive)
│ ├── home/ # Chat interface (used by both `/` and `/chat/:botId?/:sessionId?`)
│ │ ├── index.vue # Route ↔ store sync, session sidebar + chat area
│ │ ├── composables/ # Page-specific composables
│ │ │ ├── useFileManagerProvider.ts # File manager context
│ │ │ └── useMediaGallery.ts # Media gallery state
│ │ └── components/ # Chat UI components (28 files)
│ │ ├── chat-area.vue # Main chat area (messages, input, attachments)
│ │ ├── session-sidebar.vue # Session list sidebar (search, filter, CRUD)
│ │ ├── session-info-panel.vue # Session info panel
│ │ ├── chat-header.vue # Chat top bar (status, step indicator)
│ │ ├── message-item.vue # Single message (user/assistant, markdown, blocks)
│ │ ├── session-item.vue # Session list row (avatar, title, timestamp)
│ │ ├── thinking-block.vue # Collapsible thinking/reasoning block
│ │ ├── attachment-block.vue # Attachment grid (images, audio, files)
│ │ ├── media-gallery-lightbox.vue # Fullscreen media lightbox
│ │ ├── tool-call-block.vue # Generic tool call wrapper block
│ │ ├── tool-call-generic.vue # Generic tool call (name, status, JSON I/O)
│ │ ├── tool-call-list.vue # File listing tool display
│ │ ├── tool-call-read.vue # File read tool display
│ │ ├── tool-call-write.vue # File write tool display
│ │ ├── tool-call-edit.vue # File edit tool display
│ │ ├── tool-call-exec.vue # Command execution tool display
│ │ ├── tool-call-web-search.vue # Web search tool display
│ │ ├── tool-call-web-fetch.vue # Web fetch tool display
│ │ ├── tool-call-browser.vue # Browser action tool display
│ │ ├── tool-call-memory.vue # Memory read/write tool display
│ │ ├── tool-call-message.vue # Send message tool display
│ │ ├── tool-call-email.vue # Email tool display
│ │ ├── tool-call-schedule.vue # Schedule tool display
│ │ ├── tool-call-contacts.vue # Contacts tool display
│ │ ├── tool-call-subagent.vue # Sub-agent tool display
│ │ ├── tool-call-skill.vue # Skill activation tool display
│ │ ├── schedule-trigger-block.vue # Schedule trigger display
│ │ └── heartbeat-trigger-block.vue # Heartbeat trigger display
│ ├── bots/ # Bot list + detail (tabs: overview, memory, channels, etc.)
│ │ ├── index.vue # Bot grid with create dialog
│ │ ├── detail.vue # Bot detail with tabbed interface
│ │ └── components/ # Bot sub-components (27 files)
│ │ ├── bot-overview.vue # Bot overview tab
│ │ ├── bot-settings.vue # Bot settings tab
│ │ ├── bot-channels.vue # Channel configuration tab
│ │ ├── bot-memory.vue # Memory configuration tab
│ │ ├── bot-mcp.vue # MCP connections tab
│ │ ├── bot-schedule.vue # Schedule management tab
│ │ ├── bot-heartbeat.vue # Heartbeat configuration tab
│ │ ├── bot-email.vue # Email configuration tab
│ │ ├── bot-container.vue # Container management tab
│ │ ├── bot-files.vue # File browser tab
│ │ ├── bot-terminal.vue # Terminal tab
│ │ ├── bot-skills.vue # Skills tab
│ │ ├── bot-access.vue # Access control tab
│ │ ├── bot-compaction.vue # Compaction settings tab
│ │ ├── bot-card.vue # Bot card component
│ │ ├── create-bot.vue # Create bot dialog
│ │ ├── model-select.vue # Model selection dropdown
│ │ ├── model-options.vue # Model options configuration
│ │ ├── reasoning-effort-select.vue # Reasoning effort selector
│ │ ├── reasoning-effort.ts # Reasoning effort constants
│ │ ├── search-provider-select.vue # Search provider selector
│ │ ├── memory-provider-select.vue # Memory provider selector
│ │ ├── browser-context-select.vue # Browser context selector
│ │ ├── tts-model-select.vue # TTS model selector
│ │ ├── channel-settings-panel.vue # Channel settings panel
│ │ ├── container-create-progress.vue # Container creation progress
│ │ └── weixin-qr-login.vue # WeChat QR login
│ ├── providers/ # LLM provider & model management
│ ├── web-search/ # Web search provider management
│ ├── memory/ # Memory provider management
│ ├── speech/ # TTS / speech provider & model management
│ ├── email/ # Email provider management
│ ├── browser/ # Browser context management
│ ├── supermarket/ # Supermarket (template/skill marketplace)
│ ├── usage/ # Token usage statistics
│ ├── profile/ # User profile settings (password, bind codes)
│ ├── platform/ # Platform management
│ ├── about/ # About page
│ └── oauth/ # OAuth callback pages
│ └── mcp-callback.vue # MCP OAuth callback handler
├── store/ # Pinia stores
│ ├── user.ts # User state, JWT token, login/logout
│ ├── settings.ts # UI settings (theme, language)
│ ├── capabilities.ts # Server capabilities (container backend)
│ ├── chat-selection.ts # Current bot/session selection (localStorage persisted)
│ ├── chat-list.ts # Chat messages, streaming state, SSE/WS event processing
│ └── chat-list.utils.ts # Chat list utility functions (+ chat-list.utils.test.ts)
├── stores/ # Additional stores (non-core)
│ └── supermarket-mcp-draft.ts # Supermarket MCP draft state
└── utils/ # Utility functions
├── api-error.ts # API error message extraction
├── date-time.ts # Date/time formatting
├── date-time.test.ts # Date/time tests
├── channel-type-label.ts # Channel type label utilities
├── key-value-tags.ts # Tag ↔ Record conversion
├── key-value-tags.test.ts # Tag conversion tests
├── image-ref.ts # Image reference URL resolution
├── image-ref.test.ts # Image ref tests
├── timezones.ts # Timezone list and utilities
└── useControlVisibleStatus.ts # Visibility control utility
```
## Routes
The app uses a two-section layout architecture:
### Chat Section (`/`)
| Path | Name | Component | Description |
|------|------|-----------|-------------|
| `/` | home | `home/index.vue` | Home — empty state when no bot selected |
| `/chat/:botId?/:sessionId?` | chat | `home/index.vue` | Chat interface with bot + session params |
Both routes render the same `home/index.vue` component. The `home` route shows an empty state; the `chat` route auto-selects a bot and optionally a session based on URL params. URL and store state are bidirectionally synced.
### Settings Section (`/settings`)
| Path | Name | Component | Description |
|------|------|-----------|-------------|
| `/settings/bots` | bots | `bots/index.vue` | Bot list grid |
| `/settings/bots/:botId` | bot-detail | `bots/detail.vue` | Bot detail with tabs |
| `/settings/providers` | providers | `providers/index.vue` | LLM provider & model management |
| `/settings/web-search` | web-search | `web-search/index.vue` | Web search provider management |
| `/settings/memory` | memory | `memory/index.vue` | Memory provider management |
| `/settings/speech` | speech | `speech/index.vue` | TTS / speech provider & model management |
| `/settings/email` | email | `email/index.vue` | Email provider management |
| `/settings/browser` | browser | `browser/index.vue` | Browser context management |
| `/settings/supermarket` | supermarket | `supermarket/index.vue` | Template/skill marketplace |
| `/settings/usage` | usage | `usage/index.vue` | Token usage statistics |
| `/settings/profile` | profile | `profile/index.vue` | User profile settings |
| `/settings/platform` | platform | `platform/index.vue` | Platform management |
| `/settings/about` | about | `about/index.vue` | About page |
`/settings` redirects to `/settings/bots` by default.
### Standalone Routes
| Path | Name | Component | Description |
|------|------|-----------|-------------|
| `/login` | Login | `login/index.vue` | Login form (no auth required) |
| `/oauth/mcp/callback` | oauth-mcp-callback | `oauth/mcp-callback.vue` | MCP OAuth callback (no auth required) |
### Auth Guard
- All routes except `/login` and `/oauth/*` require `localStorage.getItem('token')`.
- Logged-in users accessing `/login` are redirected to `/`.
- Chunk load errors (dynamic import failures) trigger an automatic page reload.
- Tauri integration: `afterEach` hook calls `resize_for_route` via `@tauri-apps/api/core` when running inside Tauri.
## Layout System
Two-section layout architecture, both sharing the same `MainLayout` wrapper:
1. **MainLayout** (`layout/main-layout/`) — Top-level wrapper using `SidebarProvider` from `@memohai/ui`. Provides `#sidebar` and `#main` slots.
2. **Chat Section** (`pages/main-section/`) — Uses `MainLayout` with:
- **Sidebar** (`components/sidebar/`) — Bot list sidebar (collapsible). Header shows "Bots" label + create button. Body lists all bots as `BotItem` entries. Footer has a settings gear link to `/settings`.
- **MainContainer** (`components/main-container/`) — `<KeepAlive>` wrapped `<RouterView>` for chat pages.
3. **Settings Section** (`pages/settings-section/`) — Uses `MainLayout` with:
- **SettingsSidebar** (`components/settings-sidebar/`) — Collapsible settings navigation. Top has a "back to chat" button that restores the last selected bot/session. Menu items: Bots, Providers, Web Search, Memory, Speech, Email, Browser, Supermarket, Usage, Profile, About.
- **SidebarInset** — `<KeepAlive>` wrapped `<RouterView>` for settings pages.
4. **Home/Chat Page** (`pages/home/`) — Internal layout:
- **SessionSidebar** — Left panel: session search, source filter, new session button, session list.
- **ChatArea** — Center panel: message list with scroll, input area with attachments.
- **SessionInfoPanel** — Right panel: session info display.
Several settings pages use **MasterDetailSidebarLayout** (`components/master-detail-sidebar-layout/`) for left-sidebar + detail-panel patterns (providers, web search, email, memory, speech, browser).
## CSS & Theming
Design tokens, color palette, typography, elevation strategy, and component visual specs are defined in `packages/ui/DESIGN.md`. **Read that file before making any UI changes.**
### Tailwind CSS 4
CSS-based configuration (no `tailwind.config.*` file). All design tokens (CSS variables, `@theme inline` mapping, base styles) live in `packages/ui/src/style.css`. The web app imports them via:
```css
@import "@memohai/ui/style.css";
```
### Dark Mode
- Runtime: `useColorMode` from `@vueuse/core` in `store/settings.ts`
- Storage: theme preference persisted via `useStorage`
- Toggle: Available in Settings page and login page
- Usage: semantic tokens auto-switch; no `dark:` prefix needed
### Styling Rules
- Use Tailwind utility classes; avoid `<style>` blocks.
- Always use semantic color tokens (`text-foreground`, `bg-card`, `border-border`, etc.) — never hardcode raw colors (`gray-*`, `bg-white`, `text-black`).
- Follow the design system rules in `packages/ui/DESIGN.md`.
## UI Components (@memohai/ui)
All UI primitives are provided by `@memohai/ui` (43 component groups built on Reka UI). Do not import Reka UI directly. For the component design specification (variants, colors, elevation, spacing), see `packages/ui/DESIGN.md`.
- **Exception**: Physical UI knobs (Switch thumb, Slider thumb) may keep `bg-white` as they need to contrast against colored tracks regardless of theme.
- **No scoped CSS modules**: Styling is done inline via utility classes.
### CSS Imports (main.ts)
```
style.css — Tailwind + theme tokens
animate.css — Animation utilities
markstream-vue/index.css — Markdown rendering
katex/dist/katex.min.css — Math rendering
vue-sonner/style.css — Toast notifications (in App.vue)
```
`@memohai/ui` provides 43 component groups built on Reka UI primitives + Tailwind + class-variance-authority:
- **Form**: `Form`, `FormField`, `FormFieldArray`, `FormItem`, `FormControl`, `FormLabel`, `FormMessage`, `FormDescription`
- **Input**: `Input`, `Textarea`, `InputGroup` (Addon, Button, Input, Text, Textarea), `NativeSelect`, `Combobox`, `TagsInput`, `InputOTP` (Group, Slot, Separator)
- **Selection**: `Select`, `RadioGroup`, `Checkbox`, `Switch`, `Toggle`, `Slider`
- **Layout**: `Card`, `Separator`, `Sheet`, `Sidebar` (24 sub-components), `ScrollArea`, `Collapsible`, `Item` (10 sub-components)
- **Overlays**: `Dialog` (incl. `DialogScrollContent`), `Popover`, `Tooltip`, `DropdownMenu`, `ContextMenu`, `Command` (Dialog, Group, Input, Item, List)
- **Data**: `Table` (9 sub-components), `Badge`, `BadgeCount`, `Avatar`, `Skeleton`, `Empty` (5 sub-components)
- **Navigation**: `Breadcrumb`, `Tabs`, `Pagination`, `PinInput` (Group, Slot, Separator)
- **Feedback**: `Button`, `ButtonGroup` (Separator, Text), `Spinner`, `Alert`, `Toaster` (Sonner), `Kbd`
- **Effects**: `TextGenerateEffect`
### Form Pattern (vee-validate + Zod)
```vue
<script setup>
const form = useForm({
validationSchema: toTypedSchema(z.object({
name: z.string().min(1),
})),
})
</script>
<template>
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<Label>Name</Label>
<FormControl>
<Input v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</template>
```
### Icon Usage
- **Lucide** (primary): Direct component imports from `lucide-vue-next`. Example: `import { Plus, Search, Bot } from 'lucide-vue-next'``<Plus class="size-4" />`. Used for all UI icons (actions, navigation, status indicators, etc.).
- **`@memohai/icon`** (brand icons): Workspace package (`packages/icons/`) providing AI provider, search engine, and channel platform SVG icons as Vue components. Example: `import { Openai, Claude } from '@memohai/icon'`.
- **Do NOT use FontAwesome** for new code. Legacy FontAwesome usage remains only in commented-out code blocks. Always use Lucide for UI icons and `@memohai/icon` for brand logos.
### Notification Pattern
```typescript
import { toast } from 'vue-sonner'
toast.success(t('common.saved'))
toast.error(resolveApiErrorMessage(error, 'Failed'))
```
## Data Fetching
### API Client Setup (`lib/api-client.ts`)
- SDK: `@memohai/sdk` auto-generated from OpenAPI via `@hey-api/openapi-ts`
- Base URL: `VITE_API_URL` env var (defaults to `/api`, proxied by Vite dev server to backend)
- Auth: Request interceptor attaches `Authorization: Bearer ${token}` from localStorage
- 401 handling: Response interceptor removes token and redirects to `/login`
### Pinia Colada (Server State)
Primary data fetching mechanism for CRUD operations:
```typescript
// Query — auto-generated from SDK
const { data, isLoading } = useQuery(getBotsQuery())
// Custom query with dynamic key
const { data } = useQuery({
key: () => ['bot-settings', botId.value],
query: async () => {
const { data } = await getBotsByBotIdSettings({
path: { bot_id: botId.value },
throwOnError: true,
})
return data
},
enabled: () => !!botId.value,
})
// Mutation with cache invalidation
const queryCache = useQueryCache()
const { mutateAsync } = useMutation({
mutation: async (body) => {
const { data } = await putBotsByBotIdSettings({
path: { bot_id: botId.value },
body,
throwOnError: true,
})
return data
},
onSettled: () => queryCache.invalidateQueries({
key: ['bot-settings', botId.value],
}),
})
```
SDK also generates colada helpers: `getBotsQuery()`, `postBotsMutation()`, query key factories.
### Pinia Stores (Client State)
| Store | ID | Purpose |
|-------|----|---------|
| `user` | `user` | JWT token (`useLocalStorage`), user info (id, username, role, displayName, avatarUrl, timezone), login/logout |
| `settings` | `settings` | Theme (dark/light), language (en/zh), synced with `useColorMode` and vue-i18n locale |
| `capabilities` | `capabilities` | Server feature flags (container backend, snapshot support), loaded once from `getPing()` |
| `chat-selection` | `chat-selection` | Current bot ID and session ID, persisted via `useStorage` to localStorage |
| `chat-list` | `chat` | Chat messages, sessions, bots, streaming state, SSE/WS event processing. Depends on `chat-selection` store for current bot/session. Utility functions in `chat-list.utils.ts` |
Additional stores in `stores/`:
| Store | Purpose |
|-------|---------|
| `supermarket-mcp-draft` | Supermarket MCP draft state management |
Stores use Composition API style (`defineStore(() => { ... })`), with persistence via `pinia-plugin-persistedstate` or `useStorage`.
### Streaming (Chat)
Chat supports two transport modes: **Server-Sent Events (SSE)** and **WebSocket**.
#### SSE Streaming
- **Endpoints**: `/bots/{bot_id}/local/stream` (send + stream), `/messages/events` (real-time message updates)
- **Parsing**: `composables/api/useChat.sse.ts` reads `ReadableStream<Uint8Array>` and parses SSE `data:` lines
- **Events**: `text_delta`, `reasoning_delta`, `tool_call_start/end`, `attachment_delta`, `processing_completed/failed`
- **Retry**: `useRetryingStream` composable provides exponential backoff for reconnection
#### WebSocket
- **Endpoint**: `/bots/{bot_id}/local/ws` (with token query param)
- **Implementation**: `composables/api/useChat.ws.ts` wraps native `WebSocket` with send, abort, close, and auto-reconnect
- **State**: `store/chat-list.ts` processes streaming events from either transport into reactive message blocks in real-time
- **Abort**: Stream cancellation via `AbortSignal` (SSE) or close message (WS)
### Error Handling
- **Global**: `utils/api-error.ts``resolveApiErrorMessage()` extracts error from `message`, `error`, `detail` fields
- **Mutations**: `useDialogMutation` composable wraps mutations with automatic `toast.error()` on failure
- **SDK**: All calls use `throwOnError: true`; try/catch at component level
- **Streams**: `processing_failed` / `error` events appended to message blocks
## i18n
- Plugin: vue-i18n (Composition API, `legacy: false`)
- Locales: `en` (English, default), `zh` (Chinese)
- Files: `src/i18n/locales/en.json`, `src/i18n/locales/zh.json`
- Usage: `const { t } = useI18n()``t('bots.title')`
- Key namespaces: `common`, `auth`, `sidebar`, `breadcrumb`, `settings`, `about`, `chat`, `models`, `provider`, `webSearch`, `memory`, `speech`, `email`, `browser`, `mcp`, `home`, `bots`, `usage`, `supermarket`
## Vite Configuration
- Dev server port: 8082 (from `config.toml`)
- Proxy: `/api` → backend (default `http://localhost:8080`)
- Aliases: `@``./src`, `#``../ui/src`
- Config: reads from `MEMOH_CONFIG_PATH` / `CONFIG_PATH` when provided, otherwise `../../config.toml`, via `@memohai/config`
## Development Rules
- Use Vue 3 Composition API with `<script setup>` exclusively.
- Style with Tailwind utility classes; avoid `<style>` blocks. Follow the design system in `packages/ui/DESIGN.md`.
- **Always use semantic color tokens** (`text-foreground`, `bg-card`, `border-border`, `text-muted-foreground`, `bg-accent`, etc.) instead of raw colors (`gray-*`, `bg-white`, `text-black`). Never introduce hardcoded Tailwind color classes for themed elements — this breaks dark mode consistency.
- Use `@memohai/ui` components for all UI primitives; do not import Reka UI directly.
- Use `lucide-vue-next` for all UI icons. Use `@memohai/icon` for brand/provider logos. **Never use FontAwesome** — do not add `<FontAwesomeIcon>`, do not import from `@fortawesome/*`, do not use inline SVG or base64-encoded SVG in templates.
- Use Pinia Colada (`useQuery`/`useMutation`) for server state; use Pinia stores for client state only.
- API calls must go through `@memohai/sdk`; never call `fetch()` directly.
- All user-facing strings must use i18n keys (`t('key')`) — never hardcode text.
- Forms must use vee-validate + Zod schemas via `toTypedSchema()`.
- Error messages via `resolveApiErrorMessage()` + `toast.error()`.
- Page components go in `pages/{feature}/`; page-specific sub-components go in `pages/{feature}/components/`.
- Page-specific composables go in `pages/{feature}/composables/`.
- Shared components go in `components/`.
- Composables go in `composables/`; API-specific composables in `composables/api/`.