Adding a New Feature
This guide describes the end-to-end process for implementing a complete feature in Elftia. Building on Adding IPC Routes, it covers the full lifecycle including planning, testing, and documentation updates.
Overview: Feature Development Flow
Plan → Backend Implementation → Frontend Implementation → Quality Check → Testing → Documentation Update
flowchart LR
A[Planning] --> B[Backend]
B --> C[Frontend]
C --> D[Quality Check]
D --> E[Testing]
E --> F[Docs Update]
F --> G[Submit PR]
Phase 1: Planning
Before writing any code, answer the following questions clearly:
Requirements Analysis Checklist
| Question | Purpose |
|---|---|
| What shared types are needed? | Determines type files under @shared/contracts/ |
| Does it involve database table operations? | Determines whether a new migration is needed |
| What Service logic is needed? | Determines backend modules |
| What IPC channels are needed? | Determines the Router and Preload API |
| What UI components are needed? | Determines the frontend file structure |
| Does it add a new routed page? | Determines whether lazy loading and route registration are needed |
| Is it user-visible? | Determines whether the knowledge base docs need updating |
File Planning Template
Using a "Notes" feature as an example:
packages/
├── desktop/app/
│ ├── shared/contracts/
│ │ └── note-types.ts # Shared types
│ └── main/
│ ├── services/content/workspace/notes/
│ │ ├── NoteFileService.ts # File CRUD service
│ │ └── NoteWatcher.ts # File watcher service
│ └── services/routers/
│ └── NoteRouter.ts # IPC Router
│
├── renderer/src/
│ ├── features/notes/
│ │ ├── hooks/
│ │ │ └── useNotes.ts # Data fetching Hook
│ │ └── components/
│ │ ├── NotesList.tsx # List component
│ │ ├── NoteEditor.tsx # Editor component
│ │ └── index.ts # Barrel export
│ └── locales/
│ ├── en/notes.json
│ ├── zh/notes.json
│ └── ja/notes.json
Phase 2: Backend Implementation
Create backend code in the following order:
2.1 Shared Types
{/* packages/desktop/app/shared/contracts/note-types.ts */}
export interface Note {
id: string;
title: string;
content: string;
tags: string[];
createdAt: string;
updatedAt: string;
}
export interface CreateNoteInput {
title: string;
content?: string;
tags?: string[];
}
2.2 Service
Services encapsulate all business logic. Follow the single-responsibility principle — file read/write and file watching should be two separate Services.
services/content/workspace/notes/
├── NoteFileService.ts # File CRUD operations
└── NoteWatcher.ts # File system change watching
2.3 Router
Register IPC handlers using the secureHandle pattern. All parameters must pass Zod validation.
See Adding IPC Routes for detailed steps.
2.4 Registration + Preload
- Create Service and Router instances in
registerAllRouters()insiderouters/index.ts - Call
router.register() - Expose methods in the
apiobject inpreload/index.ts
Phase 3: Frontend Implementation
3.1 Create the Hook
Encapsulate data fetching and state management logic in a custom Hook:
{/* packages/renderer/src/features/notes/hooks/useNotes.ts */}
export function useNotes() {
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState(true);
// Data fetching, CRUD operations...
return { notes, loading, create, update, remove };
}
3.2 Create Components
Components are responsible for UI rendering only; delegate business logic to Hooks:
components/notes/
├── index.ts # Barrel export
├── NotesList.tsx # List view (< 400 lines)
├── NoteEditor.tsx # Editor (< 400 lines)
└── NoteCard.tsx # Single note card
Component standards:
- Use semantic tokens (
bg-surface-1,text-foreground), not hardcoded colors - Use project UI components (
Button,Input,Select), not native HTML controls - Use
useTranslation()for copy internationalization
3.3 Route Registration (for new pages)
If the feature needs a standalone page:
{/* packages/renderer/src/app/lazy/pages.tsx */}
export const LazyNotesPage = lazy(() => import('../../features/notes/NotesPage'));
export const SuspenseNotesPage = withSuspense(LazyNotesPage, PageSkeleton);
{/* packages/renderer/src/app/App.tsx — old import paths remain valid via the lazy/index.ts barrel re-export */}
import { SuspenseNotesPage as NotesPage } from './lazy';
<Route path="/notes" element={<NotesPage />} />
For adding a new workspace type (not a new page), follow a different path: edit
features/chat/components/content/workspaces/registry.ts, add adefinitions/<Name>Workspace.tsx, and add the corresponding lazy import inapp/lazy/workspaces.tsx. See the "Adding a New Workspace Type" section inpackages/renderer/CLAUDE.md.
3.4 i18n
Create translation files for all three languages and register the namespace in i18n/index.ts:
locales/
├── en/notes.json
├── zh/notes.json
└── ja/notes.json
Phase 4: Quality Check
ESLint
# Check modified files
npx eslint packages/renderer/src/features/notes/components/ packages/renderer/src/features/notes/hooks/
# Auto-fix import ordering, etc.
npx eslint packages/renderer/src/features/notes/components/ --fix
File Size Limits
| File Type | Recommended | Warning Line | Hard Limit |
|---|---|---|---|
| React component | 400 lines | 600 lines | 800 lines |
| Custom Hook | 300 lines | 400 lines | 600 lines |
| Utility function | 150 lines | 200 lines | 300 lines |
| Type definition | 100 lines | 150 lines | 200 lines |
| Service | 300 lines | 400 lines | 600 lines |
Files that exceed the warning line must be split within the current task — do not defer.
TypeScript
npm run typecheck
Phase 5: Testing
Test Commands
| Command | Description |
|---|---|
npm run test | Full Vitest test run |
npm run test -- --filter notes | Filter tests by keyword |
Manual Testing Checklist
- The happy path of the feature works correctly
- Error cases show correct messages (network errors, database errors, etc.)
- UI displays correctly in dark and light mode
- UI displays correctly with wallpaper transparency enabled
- Layout does not break when the window is shrunk to minimum width
Phase 6: Documentation Updates
Required Documentation Updates
| Condition | Documentation to Update |
|---|---|
| Added a Service / component / Hook / Context | .claude/skills/architecture-index/SKILL.md |
| Added a user-visible page feature | docs/elfi-kb/ knowledge base docs |
| Added a page route | docs/elfi-kb/INDEX.md route map |
| Changed an IPC interface | This doc site's IPC Channel List |
Common Pattern Reference
Pattern 1: CRUD Service
Suitable for entity management features (notes, favorites, templates, etc.).
Shared types → Service(CRUD) → Router(Zod validation) → Preload → Hook(useState+CRUD) → List + edit components
Pattern 2: Streaming Events
Suitable for features that require real-time push (chat, generation tasks, etc.).
Backend Service emits event → Main process webContents.send(channel, data) → Preload onXxx listener → Hook subscribe/unsubscribe
{/* Preload side — event subscription pattern */}
onEvent: (cb: (data: any) => void) => {
const channel = 'myFeature:event';
const handler = (_event: IpcRendererEvent, data: any) => cb(data);
ipcRenderer.on(channel, handler);
return () => ipcRenderer.removeListener(channel, handler);
},
{/* Hook side — subscription management */}
useEffect(() => {
const unsubscribe = window.api.myFeature.onEvent((data) => {
setMessages((prev) => [...prev, data]);
});
return unsubscribe;
}, []);
Pattern 3: File Operations
Suitable for features that need to read or write user files (project management, exports, etc.).
Frontend passes file path/config → Backend Service operates on the file system → Returns result URL or content
Key principle: all file system operations are completed on the backend. The frontend only passes paths and parameters; it does not operate on files directly.
Complete Checklist
Backend
- Shared types defined in
@shared/contracts/ - Service encapsulates all business logic
- Router uses
secureHandle+ Zod validation - Router registered in
registerAllRouters() - Preload API method signatures are correct
-
DesktopApitype interface is updated
Frontend
- Custom Hook encapsulates data fetching logic
- Components use semantic tokens and project UI components
- New pages use
React.lazylazy loading - i18n files created for all three languages
Quality
-
npm run lintpasses with no errors -
npm run typecheckpasses - File sizes are within limits
- Dark/light mode tests pass
- Wallpaper transparency mode tests pass
Documentation
-
architecture-indexSkill updated -
elfi-kbknowledge base updated (if user-visible feature added) - PR description clearly explains the changes