
Architecture
- 39 installs
- 31.4k repo stars
- Updated August 5, 2026
- iofficeai/aionui
Decide where new code belongs in the AionUi Electron multi-process project using its file-structure conventions and decision tree.
About
Defines file placement and structure conventions for the AionUi Electron multi-process project across renderer, main, and shared layers. A developer uses it when creating new files, adding bridges/services/workers, or reviewing code for structure compliance.
- Decision tree for where new code goes in an Electron multi-process project
- References for renderer, main/shared process, and monorepo layout
Architecture by the numbers
- 39 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #878 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/iofficeai/aionui --skill architectureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 39 |
|---|---|
| repo stars | ★ 31.4k |
| Last updated | August 5, 2026 |
| Repository | iofficeai/aionui ↗ |
What it does
Decide where new code belongs in the AionUi Electron multi-process project using its file-structure conventions and decision tree.
Files
Architecture Skill
Determine correct file placement and structure for an Electron multi-process project.
Detailed References
- Renderer layer (components, hooks, utils, pages, CSS): references/renderer.md
- Main process & shared layer (bridges, services, worker, preload): references/process.md
- Project root & monorepo layout (directory structure, migration status): references/project-layout.md
---
Decision Tree — Where Does New Code Go?
Is it UI (React components, hooks, pages)?
└── YES → packages/desktop/src/renderer/ → see references/renderer.md
Is it an IPC handler responding to renderer calls?
└── YES → packages/desktop/src/process/bridge/ → see references/process.md
Is it business logic running in the main process?
└── YES → packages/desktop/src/process/services/ → see references/process.md
Is it an AI platform connection (API client, message protocol)?
└── YES → packages/desktop/src/process/agent/<platform>/
Is it a background task that runs in a worker thread?
└── YES → packages/desktop/src/process/worker/
Is it used by BOTH main and renderer processes?
└── YES → packages/desktop/src/common/
Is it an HTTP/WebSocket endpoint?
└── YES → packages/desktop/src/process/webserver/
Is it a plugin/extension resolver or loader?
└── YES → packages/desktop/src/process/extensions/
Is it a messaging channel (Lark, DingTalk, Telegram)?
└── YES → packages/desktop/src/process/channels/---
Process Boundary Rules
Hard rules — violating them causes runtime crashes.
| Process | Can use | Cannot use |
|---|---|---|
Main (packages/desktop/src/process/) | Node.js, Electron main APIs, fs, path, child_process | DOM APIs (document, window, React) |
Renderer (packages/desktop/src/renderer/) | DOM APIs, React, browser APIs | Node.js APIs (fs, path), Electron main APIs |
Worker (packages/desktop/src/process/worker/) | Node.js APIs | DOM APIs, Electron APIs |
Preload (packages/desktop/src/preload/) | contextBridge, ipcRenderer | DOM manipulation, Node.js fs |
Cross-process communication:
- Main ↔ Renderer: IPC via
packages/desktop/src/preload/+packages/desktop/src/process/bridge/*.ts - Main ↔ Worker: fork protocol via
packages/desktop/src/process/worker/WorkerProtocol.ts
// NEVER in renderer
import { something } from '@process/services/foo'; // crashes at runtime
// Use IPC instead
const result = await window.api.someMethod(); // goes through preload---
Naming Conventions
Directories
| Scope | Convention | Reason |
|---|---|---|
| Renderer component/module dirs | PascalCase | React convention — dir name = component name |
| Everything else | lowercase | Node.js convention |
| Categorical dirs (everywhere) | lowercase | components/, hooks/, utils/, services/ |
| Platform dirs (everywhere) | lowercase | acp/, codex/, gemini/ — cross-process consistency |
Quick test: "Inside packages/desktop/src/renderer/ AND represents a specific component/feature (not a category)?" → PascalCase. Otherwise → lowercase.Files
| 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 |
---
Structural Rules
1. Directory size limit: Max 10 direct children. Split into subdirectories by responsibility when approaching. 2. No single-file directories: Merge into parent or related directory. 3. Single file vs directory: If a component needs a private sub-component or hook, convert to a directory with index.tsx. 4. Page-private first: Start code in pages/<PageName>/. Promote to shared only when a second consumer appears.
Test File Mapping
Tests mirror source files in tests/ subdirectories:
| Source | Test |
|---|---|
packages/desktop/src/process/services/CronService.ts | tests/unit/cronService.test.ts |
packages/desktop/src/renderer/hooks/ui/useAutoScroll.ts | tests/unit/useAutoScroll.dom.test.ts |
packages/desktop/src/process/extensions/ExtensionLoader.ts | tests/unit/extensions/extensionLoader.test.ts |
When tests/unit/ exceeds 10 direct children, group into subdirectories matching source structure.
---
Quick Checklist
- [ ] Code is in the correct process directory (no cross-process imports)
- [ ] Renderer code does not use Node.js APIs
- [ ] Main process code does not use DOM APIs
- [ ] New IPC channels are bridged through
preload.ts - [ ] Renderer component/module dirs use PascalCase; categorical dirs use lowercase
- [ ] Platform dirs use lowercase everywhere
- [ ] Directory-based modules have
index.tsx/index.tsentry point - [ ] Page-private code is under
pages/<PageName>/, not in shared dirs - [ ] No single-file directories
- [ ] No directory exceeds 10 direct children
- [ ] New source files are auto-included in coverage — verify they are not accidentally excluded in
vitest.config.ts→coverage.exclude - [ ] New services separate pure logic from IO
Main Process & Shared Layer
packages/desktop/src/process/ Structure
packages/desktop/src/process/
├── bridge/ # IPC handlers — one file per domain
│ ├── index.ts # Registers all bridges
│ └── *Bridge.ts # Individual bridge files
├── services/ # Business logic services
│ ├── cron/ # Complex service → subdirectory
│ └── mcp-services/
├── database/ # SQLite layer — schema, migrations, repositories
├── task/ # Agent/task management — managers, factories
├── utils/ # Main-process-only utilities
└── i18n/ # Main-process i18nNaming Conventions
| Type | Pattern | Examples |
|---|---|---|
| Bridge | <domain>Bridge.ts (camelCase) | cronBridge.ts, webuiBridge.ts |
| Service | <Name>Service.ts (PascalCase) | CronService.ts, McpService.ts |
| Service interface | I<Name>Service.ts | IConversationService.ts |
| Repository | <Name>Repository.ts | SqliteConversationRepository.ts |
| Agent Manager | <Platform>AgentManager.ts | AcpAgentManager.ts |
All directories use lowercase (Node.js convention):
packages/desktop/src/process/
├── bridge/ # lowercase
├── services/ # lowercase
│ ├── cron/ # lowercase
│ └── mcp-services/ # lowercase (kebab-case for multi-word)
├── database/ # lowercase
└── task/ # lowercaseAdding a New IPC Bridge
1. Create packages/desktop/src/process/bridge/<domain>Bridge.ts 2. Register in packages/desktop/src/process/bridge/index.ts 3. Expose channel in packages/desktop/src/preload/ 4. Add renderer-side types if needed
Adding a New Service
- Simple → single file in
packages/desktop/src/process/services/ - Complex (multiple files) → subdirectory:
packages/desktop/src/process/services/<name>/
Service Testability Rules
Pure Logic vs IO Separation
- Pure logic (transformation, validation, formatting) → standalone functions, no
fs/db/net - IO operations (file read, DB query, HTTP call) → thin wrappers in service class or repository
- Service methods should receive IO results as parameters
Dependency Injection
// ❌ Hard to test
import { db } from '@process/database';
function getConversation(id: string) {
return db.query('SELECT * FROM conversations WHERE id = ?', id);
}
// ✅ Easy to test
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.
---
Shared Layer
Preload (packages/desktop/src/preload/)
IPC bridge between main and renderer. Uses contextBridge to expose safe APIs.
- All main ↔ renderer communication goes through this file
- Only
contextBridgeandipcRendererAPIs allowed - No DOM manipulation, no Node.js
fs
Common (packages/desktop/src/common/)
Code imported by both main and renderer processes.
- Belongs: shared types, API adapters, protocol converters, storage keys
- Does NOT belong: React components →
renderer/, Node.js-specific →process/
Agent (packages/desktop/src/process/agent/)
One directory per AI platform (lowercase): acp/, codex/, gemini/, nanobot/, openclaw/. Each has index.ts entry. Runs in main or worker process.
Worker (packages/desktop/src/process/worker/)
packages/desktop/src/process/worker/
├── fork/ # Fork management
├── <platform>.ts # One file per agent platform (lowercase)
├── WorkerProtocol.ts # Protocol definition (PascalCase — it's a class)
└── index.tsOther Modules
| Module | Location | Purpose |
|---|---|---|
| Channels | packages/desktop/src/process/channels/ | Multi-channel messaging (Lark, DingTalk, Telegram) |
| Extensions | packages/desktop/src/process/extensions/ | Plugin loading, resolvers, sandbox |
| WebServer | packages/desktop/src/process/webserver/ | Express + WebSocket for WebUI |
| Adapter | packages/desktop/src/common/adapter/ | Platform adapters (browser vs main environment) |
Project Layout
Root Directory
Rules
- Workspace root stays minimal: root keeps shared config, scripts, tests, docs, assets, and package manager files.
- Desktop app source lives under `packages/desktop/`: do not add new app runtime code back to the root.
- README translations →
docs/readme/, not root. Only mainreadme.mdstays at root. - Guide documents (
*_GUIDE.md,CODE_STYLE.md) →docs/ - Build artifacts (
out/,node_modules/) are gitignored
Current Root Structure (M1)
project-root/
├── packages/
│ └── desktop/ # Electron desktop workspace
├── tests/ # Shared test suites
├── docs/ # All documentation
├── scripts/ # Build and tooling scripts
├── resources/ # Static resources (icons, images, installers)
├── public/ # Shared Vite public assets
├── patches/ # npm/bun patches
├── homebrew/ # Homebrew formula
├── package.json # Workspace root config
├── tsconfig.json # Shared TS config
├── vitest.config.ts # Shared test config
├── AGENTS.md # Agent conventions
├── CLAUDE.md # Claude-specific config
└── ... # Other root-level tooling configMigration rule: New desktop runtime modules go under packages/desktop/, not the repository root.---
packages/desktop/ Layout
Workspace Structure
packages/desktop/
├── src/
│ ├── renderer/ # Renderer layer — React UI, no Node.js APIs
│ ├── process/ # Main process layer — Node.js / Electron business logic
│ ├── common/ # Shared cross-process code
│ ├── preload/ # IPC bridge entrypoints
│ ├── index.ts # Main process entry
│ └── types.d.ts # Ambient declarations
├── electron.vite.config.ts
├── electron-builder.yml
└── package.jsonpackages/desktop/src/ Structure
packages/desktop/src/
├── renderer/ # React UI, browser-only code
├── process/ # Electron main-process and worker code
│ ├── bridge/ # IPC handlers
│ ├── services/ # Business logic
│ ├── agent/ # AI platform connections
│ ├── channels/ # Multi-channel messaging
│ ├── extensions/ # Plugin system
│ ├── webserver/ # WebUI server
│ └── worker/ # Background workers
├── common/ # Shared types, adapters, utilities
├── preload/ # contextBridge / ipcRenderer exposure
├── index.ts # Main process entry point
└── types.d.ts # Ambient declarationsPlacement Rules
- New Electron runtime code belongs in
packages/desktop/src/**. - Root-level scripts and config may reference
packages/desktop/**, but should not duplicate app source. - Tests remain under
tests/**and should reference desktop source through aliases orpackages/desktop/...paths.
Renderer Layer (packages/desktop/src/renderer/)
Root Directory — Standard Layout
At most 3 entry files + 7 directories = 10 items:
packages/desktop/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 URLsDoes NOT belong at renderer root:
- CSS files →
styles/ - Component files (
.tsx) →components/orpages/ - Single-file directories → merge into a related directory
UI Library & Icon Standards
- Components:
@arco-design/web-react— use Arco components first - Icons:
@icon-park/react— all icons from this library - No raw HTML for interactive elements (
<button>,<input>,<select>, etc.) — use Arco equivalents - Layout tags (
<div>,<span>,<section>, etc.) may be used freely
CSS Conventions
- Prefer UnoCSS utility classes (
flex items-center gap-8px) - Complex/reusable styles: CSS Modules (
ComponentName.module.css). No plain.cssfor components - Semantic color tokens only: Use
uno.config.tstokens (text-t-primary,bg-base,border-b-base) or CSS variables. No hardcoded colors. Exception:CssThemeSettings/presets/ - No inline styles except dynamically computed values
- Arco overrides: In component's CSS Module via
:global(.arco-xxx). No global override files - Global styles: Only in
packages/desktop/src/renderer/styles/
components/ — Layered Structure
Two layers:
Fixed layer:
base/— Generic UI primitives (Modal, Select, ScrollArea). No business logic, no app-specific context
Business layer:
- Subdirectories by business domain (lowercase). Create when ≥ 2 shared components belong to the same domain
- Single component may stay at
components/root until a second same-domain component appears
Constraints:
- Root ≤ 10 direct children
base/must not depend on business logic- Single-page components →
pages/<PageName>/components/
packages/desktop/src/renderer/components/
├── base/ # UI primitives
├── chat/ # Conversation/message domain
├── agent/ # Agent selection/configuration
├── settings/ # Settings domain
├── layout/ # Window frame and layout
├── media/ # File preview, image viewer
└── ... # New domains as neededhooks/ — Grouping by Business Domain
Group into subdirectories when exceeding 10 children. Generic hooks stay at root.
hooks/
├── agent/ # Agent/model — useModelProviderList, useAgentReadinessCheck
├── chat/ # Chat/message — useAutoTitle, useSendBoxDraft, useSlashCommands
├── file/ # File/workspace — useDragUpload, useOpenFileSelector
├── mcp/ # MCP related
├── ui/ # Generic UI — useAutoScroll, useDebounce, useResizableSplit
├── system/ # System-level — useDeepLink, useTheme, usePwaMode
└── index.ts # Public re-exports (optional)utils/ — Grouping by Business Domain
Same principle as hooks. Group when exceeding 10 children.
utils/
├── file/ # File handling — base64, fileType, download
├── workspace/ # Workspace — workspace, workspaceEvents, workspaceFs
├── chat/ # Chat/message — chatMinimapEvents, diffUtils, latexDelimiters
├── model/ # Model/agent — agentLogo, modelCapabilities, modelContextLimits
├── theme/ # Theme/style — customCssProcessor, themeCssSync
├── ui/ # Generic UI — clipboard, focus, siderTooltip, HOC
├── common.ts # Misc utilities
├── emitter.ts
└── platform.tsPage Module Structure
PageName/ # PascalCase
├── index.tsx # Entry point (required)
├── components/ # Page-private components (lowercase categorical dir)
│ ├── FeatureA.tsx # Simple sub-component
│ └── FeatureB/ # Complex sub-component (PascalCase)
│ └── index.tsx
├── hooks/ # Page-private hooks
├── contexts/ # Page-private React contexts
├── utils/ # Page-private utilities
├── types.ts
└── constants.tsOnly create sub-directories you need. Use these exact names.
Page-Level Directory Naming
| Type | Convention | Examples |
|---|---|---|
| Categorical (standard role) | lowercase | components/, hooks/, context/, utils/ |
| Feature module (business) | PascalCase | GroupedHistory/, Workspace/, Preview/ |
| Platform directory | lowercase | acp/, codex/, gemini/ (mirrors packages/desktop/src/process/agent/) |
Example
packages/desktop/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
│ └── components/ # categorical → lowercase
└── hooks/ # categorical → lowercaseShared vs Page-Private Code
| Scope | Location |
|---|---|
| Used by one page | pages/<PageName>/components/, hooks/, etc. |
| Used by multiple pages | packages/desktop/src/renderer/components/, packages/desktop/src/renderer/hooks/ |
Promotion rule: Start page-private. Move to shared only when a second consumer appears.
Component Entry Points
- Directory-based components must have
index.tsxas the public entry point - Do not import internal files from outside the directory