docs/contributing/file-structure.md
Rules for organizing files and directories across the entire Electron project.
docs/readme/, not at root. Only the main readme.md stays at root (GitHub convention)docs/guides/docs/contributing/docs/architecture/ (research notes under docs/architecture/research/)docs/specs/ (or docs/prds/ for formal PRDs maintained by the product team)tsconfig.json, package.json, etc.) stay at root — Node.js/Electron ecosystem conventiondocs/ subdirectory, not at project root| Action | Files |
|---|---|
Move readme translations to docs/readme/ | readme_{ch,es,jp,ko,pt,tr,tw}.md |
src/)AionUi is a multi-process Electron app with three core layers: renderer, main process, and preload/shared.
src/
├── renderer/ # Renderer layer — React UI, no Node.js APIs
├── process/ # Main process layer — all Node.js / Electron business
│ ├── bridge/ # IPC handlers
│ ├── services/ # Business logic
│ ├── database/ # SQLite
│ ├── task/ # Agent/task management
│ ├── agent/ # AI platform connections
│ ├── channels/ # Multi-channel messaging
│ ├── extensions/ # Plugin system
│ ├── webserver/ # WebUI server
│ ├── worker/ # Background workers (fork)
│ └── i18n/ # Main-process i18n
├── common/ # Shared layer — cross-process types, adapters, utilities
├── preload.ts # IPC bridge — contextBridge between main ↔ renderer
└── index.ts # Main process entry point
All main-process modules now live under src/process/. The src/ root contains only the three core layers (renderer/, process/, common/), the entry files (index.ts, preload.ts), and the ambient type declaration (types.d.ts).
This project straddles two ecosystems. Each follows its own convention:
| Scope | Directory naming | Reason |
|---|---|---|
Renderer (src/renderer/) | PascalCase for component/module dirs | React ecosystem — directory name = component name |
| Everything else | lowercase | Node.js ecosystem |
| Categorical dirs (everywhere) | lowercase | components/, hooks/, utils/, services/ are categories, not entities |
| Platform dirs (renderer pages) | lowercase | Mirror src/process/agent/<platform>/ naming for cross-process consistency |
"Is this directory inside
src/renderer/AND does it represent a specific component or feature module (not a category)?"YES → PascalCase. NO → lowercase.
Exception: Platform directories (
acp/,codex/,gemini/,nanobot/,openclaw/) always use lowercase, even inside renderer, to matchsrc/process/agent/.
src/renderer/
├── components/ # categorical → lowercase
│ ├── SettingsModal/ # component → PascalCase
│ └── EmojiPicker/ # component → PascalCase
├── pages/ # categorical → lowercase
│ ├── settings/ # top-level page → lowercase (route segment)
│ │ ├── CssThemeSettings/ # feature module → PascalCase
│ │ └── McpManagement/ # feature module → PascalCase
│ └── conversation/ # top-level page → lowercase
│ ├── GroupedHistory/ # feature module → PascalCase
│ ├── Workspace/ # feature module → PascalCase
│ ├── acp/ # platform dir → lowercase (mirrors src/agent/acp/)
│ └── components/ # categorical → lowercase
└── hooks/ # categorical → lowercase
src/process/services/cron/ # lowercase
src/process/agent/acp/ # lowercase
src/process/channels/plugins/dingtalk/ # lowercase
| Content | Convention | Examples |
|---|---|---|
| React components, classes | PascalCase | SettingsModal.tsx, CronService.ts |
| Hooks | camelCase with use prefix | useTheme.ts, useCronJobs.ts |
| Utilities, helpers | camelCase | formatDate.ts, cronUtils.ts |
| Entry points | index.ts / index.tsx | Required for directory-based modules |
| Config, types, constants | camelCase | types.ts, constants.ts |
| Styles | kebab-case or Name.module.css | chat-layout.css |
Violating these causes runtime crashes.
| Process | Can use | Cannot use |
|---|---|---|
Main (src/process/) | Node.js, Electron main APIs | DOM APIs, React |
Renderer (src/renderer/) | DOM APIs, React, browser APIs | Node.js APIs, Electron main APIs |
Worker (src/process/worker/) | Node.js APIs | DOM APIs, Electron APIs |
Cross-process communication MUST go through:
src/preload.ts + src/process/bridge/*.tssrc/process/worker/WorkerProtocol.ts| Type | Pattern | Examples |
|---|---|---|
| Bridge | <domain>Bridge.ts | cronBridge.ts, webuiBridge.ts |
| Service | <Name>Service.ts | CronService.ts, McpService.ts |
| Interface | I<Name>Service.ts | IConversationService.ts |
| Repository | <Name>Repository.ts | SqliteConversationRepository.ts |
Services must separate pure logic from IO operations:
fs/db/net importsServices and bridges that depend on external resources (DB, file system, other services) should accept dependencies as constructor/function parameters:
// ❌ Hard to test — must mock the entire module
import { db } from '@process/database';
function getConversation(id: string) {
return db.query('SELECT * FROM conversations WHERE id = ?', id);
}
// ✅ Easy to test — inject the dependency
function getConversation(repo: IConversationRepository, id: string) {
return repo.findById(id);
}
For existing code using direct imports, vi.mock() is acceptable. For new code, prefer parameter injection.
Test files must mirror the source file they test:
| Source | Test |
|---|---|
src/process/services/CronService.ts | tests/unit/cronService.test.ts |
src/process/bridge/fsBridge.ts | tests/unit/fsBridge.test.ts |
src/renderer/utils/chat/latexDelimiters.ts | tests/unit/latexDelimiters.test.ts |
src/renderer/hooks/ui/useAutoScroll.ts | tests/unit/useAutoScroll.dom.test.ts |
src/process/extensions/ExtensionLoader.ts | tests/unit/extensions/extensionLoader.test.ts |
When tests/unit/ exceeds 10 direct children, group into subdirectories matching the source structure (e.g., tests/unit/extensions/). New source files with logic should be added to vitest.config.ts → coverage.include.
A single directory must not contain more than 10 direct children (files + subdirectories). When a directory approaches this limit, split its contents into subdirectories grouped by responsibility.
@arco-design/web-react. All new UI must use Arco components first.@icon-park/react. All icons must come from this library.<button>, <input>, <select>, <textarea>, <modal>, etc. Use the corresponding Arco component (Button, Input, Select, Modal, etc.).<div>, <span>, <section>, <nav>, <main>, and other pure layout/semantic tags may be used freely.flex items-center gap-8px).ComponentName.module.css). Plain .css files are not allowed for component styles.uno.config.ts (e.g., text-t-primary, bg-base, border-b-base) or CSS variables. Hardcoded color values are forbidden (e.g., #86909C, rgb(0,0,0)). Exception: theme preset files under src/renderer/pages/settings/CssThemeSettings/presets/ may use hardcoded values since they define the theme tokens themselves.style={{}} except for dynamically computed values (e.g., calculated widths, positions).:global(.arco-xxx). Do not use a global override file.src/renderer/styles/ (themes, reset, layout base). No CSS files directly in src/renderer/ root.The renderer root must contain at most 3 entry files + 7 directories = 10 items.
src/renderer/
├── index.html # Vite HTML entry
├── main.tsx # React mount + app bootstrap
├── types.d.ts # Ambient type declarations
├── pages/ # Page-level modules (business code goes here)
├── components/ # Shared UI components (used across multiple pages)
├── hooks/ # Shared React hooks (supports business domain subdirs)
├── context/ # Global React contexts
├── services/ # Client-side services + i18n
├── utils/ # Utility functions + types + constants
├── styles/ # Global styles + theme configuration
└── assets/ # Static assets — Vite resolves to hashed URLs
What does NOT belong at the renderer root:
styles/.tsx) → move to components/ or pages/index.tsx entry pointpages/<PageName>/; move to shared only when a second consumer appearssrc/renderer/components/ Structurecomponents/ is for shared components used across multiple pages. It has two layers:
Fixed layer:
base/ — Generic UI primitives with no business logic. The only fixed subdirectory. Components here must not depend on app-specific context or domain logic.Business layer:
components/ root temporarily until a second component in the same domain appearsConstraints:
components/ root must not exceed 10 direct children (files + directories)pages/<PageName>/components/, not heresrc/renderer/components/
├── base/ # UI primitives — AionModal, AionSelect, FlexFullContainer, etc.
├── chat/ # Conversation/message domain (example, not exhaustive)
├── agent/ # Agent selection/configuration domain
├── settings/ # Settings domain
├── layout/ # Window frame and layout
├── media/ # File preview, image viewer
└── index.ts # Public re-exports (optional)
The business subdirectory list above is illustrative. New domains are created as needed following the same rules.
src/renderer/hooks/ — Grouping by Business DomainWhen hooks/ exceeds 10 direct children, group hooks into business domain subdirectories. Generic hooks with no clear domain stay at the root. The root must stay ≤ 10 direct children.
hooks/
├── agent/ # Agent/model related
├── chat/ # Chat/message input
├── file/ # File/workspace
├── mcp/ # MCP related
├── ui/ # Generic UI interaction
├── system/ # System-level (deep link, notification, theme, etc.)
└── index.ts # Public re-exports (optional)
Domain names are recommendations. Create new domains as needed following the same pattern.
src/renderer/utils/ — Grouping by Business DomainSame principle as hooks/. When utils/ exceeds 10 direct children, group into domain subdirectories. The root must stay ≤ 10 direct children.
utils/
├── file/ # File handling
├── workspace/ # Workspace utilities
├── chat/ # Chat/message utilities
├── model/ # Model/agent utilities
├── theme/ # Theme/style utilities
├── ui/ # Generic UI utilities
└── ... # Ungrouped utilities at root
PageName/ # PascalCase
├── index.tsx # Entry point (required)
├── components/ # lowercase (categorical)
├── hooks/ # lowercase (categorical)
├── contexts/ # lowercase (categorical)
├── utils/ # lowercase (categorical)
├── types.ts
└── constants.ts
Inside a page module (e.g., pages/conversation/), three types of subdirectories exist:
| Type | Convention | Examples |
|---|---|---|
| Categorical (standard role) | lowercase | components/, hooks/, context/, utils/ |
| Feature module (business feature) | PascalCase | GroupedHistory/, Workspace/, Preview/ |
Platform directory (mirrors src/process/agent/) | lowercase | acp/, codex/, gemini/, nanobot/, openclaw/ |
Platform directories are an exception to PascalCase. They use lowercase for cross-process naming consistency with src/process/agent/<platform>/.