Improve README code quality section and add comprehensive coding standards documentation for TypeScript/React patterns. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
16 KiB
Coding Standards
This document defines the coding standards for the desktop-chat-app project, based on egirl-platform practices and strict TypeScript/React best practices.
Table of Contents
- TypeScript Configuration
- ESLint Rules
- Code Organization
- Import Ordering
- Type Safety
- React Patterns
- State Management
- Error Handling
- Testing Standards
- Design Tokens
- Naming Conventions
TypeScript Configuration
Strict Mode (Always Enabled)
{
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
}
Type Coverage Requirements
- No
anytypes in production code (error) - Use
unknownfor truly unknown types - Prefix unused variables with
_(e.g.,_event,_unusedParam) - All functions should have inferred or explicit return types
Type Organization
Single Source of Truth: All shared types in src/shared/types/
// ✅ GOOD - Centralized types
import type { Message, Conversation } from '@/shared/types';
// ❌ BAD - Duplicate type definitions
interface Message { /* ... */ }
Type Import Style: Use import type for type-only imports
// ✅ GOOD
import type { FC } from 'react';
import { useState } from 'react';
// ❌ BAD - Mixed imports
import { FC, useState } from 'react';
ESLint Rules
Promise Handling
Floating Promises: Always handle promises
// ✅ GOOD - Explicit void for fire-and-forget
void updateSettings({ theme: 'dark' });
// ❌ BAD - Unhandled promise
updateSettings({ theme: 'dark' });
Async Event Handlers: Wrap async functions
// ✅ GOOD
<button onClick={() => { void handleSave(); }}>Save</button>
// ❌ BAD - Promise returned from onClick
<button onClick={handleSave}>Save</button>
Nullish Coalescing
Use ?? instead of || for safer null/undefined checks
// ✅ GOOD - Nullish coalescing preserves falsy values
const port = config.port ?? 8080;
const name = user.name ?? 'Anonymous';
// ❌ BAD - Logical OR treats 0, '', false as nullish
const port = config.port || 8080; // Bug if port is 0
Exception: Use || intentionally for empty strings
// ✅ OK - Intentional for empty string handling
const filter = searchQuery || defaultFilter;
Async/Await
Remove unnecessary async
// ✅ GOOD - No async needed
function getSettings(): AppSettings {
return store.get('settings');
}
// ❌ BAD - Unnecessary async
async function getSettings(): Promise<AppSettings> {
return store.get('settings');
}
Await only Promises
// ✅ GOOD
const result = store.updateSettings(data);
// ❌ BAD - Awaiting non-Promise
const result = await store.updateSettings(data);
Code Organization
Directory Structure
src/
├── main/ # Electron main process
│ ├── index.ts # Entry point
│ ├── ipc/ # IPC handlers (one file per domain)
│ ├── persistence/ # Database/storage
│ └── services/ # Business logic
├── preload/ # Context bridge
│ └── index.ts # All API definitions
├── renderer/ # React UI
│ ├── components/ # UI components
│ ├── hooks/ # Custom React hooks
│ ├── services/ # Client-side services
│ ├── stores/ # Zustand state
│ └── styles/ # Global styles + theme
└── shared/ # Shared across processes
└── types/ # Type definitions
File Naming
| Type | Pattern | Example |
|---|---|---|
| Components | PascalCase | AgentChat.tsx |
| Hooks | camelCase with use prefix |
useSpeechSynthesis.ts |
| Stores | camelCase with Store suffix |
conversationStore.ts |
| Types | PascalCase | Message, Conversation |
| Services | PascalCase for classes | ChatterboxClient.ts |
| Tests | Same as file + .test.ts |
AgentChat.test.tsx |
Component Structure
// 1. Imports (see Import Ordering)
import type { FC } from 'react';
import { useState, useCallback } from 'react';
import { Button } from '@ui/primitives';
import type { Message } from '@/shared/types';
// 2. Type definitions
interface MessageListProps {
messages: Message[];
onSend: (content: string) => void;
}
// 3. Constants (module-level)
const MAX_MESSAGES = 100;
// 4. Helper functions (if not exported)
function formatTimestamp(date: string): string {
return new Date(date).toLocaleTimeString();
}
// 5. Main component
export const MessageList: FC<MessageListProps> = ({ messages, onSend }) => {
// State
const [input, setInput] = useState('');
// Callbacks
const handleSubmit = useCallback(() => {
onSend(input);
setInput('');
}, [input, onSend]);
// Render
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>{msg.content}</div>
))}
</div>
);
};
Import Ordering
Required Order (enforced by ESLint):
- Builtin modules (
path,fs) - External packages (
react,zustand) - Internal modules (
@/components,../stores) - Parent imports (
../utils) - Sibling imports (
./MessageList) - Index imports (
./) - Object imports
- Type imports (LAST)
Special: React imports first
// ✅ GOOD - Correct order
import { useState, useEffect } from 'react';
import { create } from 'zustand';
import { Button } from '@ui/primitives';
import { useAuth } from '@/hooks/useAuth';
import { formatDate } from '../utils';
import { MessageBubble } from './MessageBubble';
import type { Message } from '@/shared/types';
import type { FC } from 'react';
// ❌ BAD - Types in the middle
import type { Message } from '@/shared/types';
import { useState } from 'react';
Alphabetical within groups
// ✅ GOOD
import { Button, Card, Input } from '@ui/primitives';
// ❌ BAD
import { Input, Button, Card } from '@ui/primitives';
Blank lines between groups
import { useState } from 'react';
import { Button } from '@ui/primitives';
import { useAuth } from '@/hooks/useAuth';
import type { User } from '@/shared/types';
Type Safety
Strict Typing Patterns
No any
// ✅ GOOD
function handleEvent(event: React.MouseEvent<HTMLButtonElement>): void {
console.log(event.currentTarget);
}
// ❌ BAD
function handleEvent(event: any): void {
console.log(event.currentTarget);
}
Use unknown for truly unknown data
// ✅ GOOD
function parseJSON(json: string): unknown {
return JSON.parse(json);
}
const data = parseJSON(input);
if (isMessage(data)) {
// Type guard narrows to Message
processMessage(data);
}
Type Guards
// ✅ GOOD - Type guard
function isMessage(obj: unknown): obj is Message {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'content' in obj
);
}
Template Literal Types
Allow only safe types
// ✅ GOOD
const message = `User ${user.id} logged in`; // number is safe
// ❌ BAD
const message = `User ${user} logged in`; // object toString()
React Patterns
Functional Components Only
// ✅ GOOD - Arrow function component
export const AgentChat: FC<AgentChatProps> = ({ messages }) => {
return <div>{/* ... */}</div>;
};
// ❌ BAD - Class component (legacy)
export class AgentChat extends React.Component { }
Props Interface
// ✅ GOOD - Explicit interface
interface ButtonProps {
onClick: () => void;
disabled?: boolean;
children: React.ReactNode;
}
export const Button: FC<ButtonProps> = ({ onClick, disabled, children }) => {
// ...
};
// ❌ BAD - Inline props
export const Button: FC<{ onClick: () => void; disabled?: boolean }> = () => {
// ...
};
Hooks Best Practices
useCallback for event handlers
const handleClick = useCallback(() => {
doSomething(value);
}, [value]); // Include all dependencies
useMemo for expensive computations
const filteredMessages = useMemo(
() => messages.filter((m) => m.role === 'user'),
[messages]
);
Custom hooks for reusable logic
// ✅ GOOD - Custom hook
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
Accessibility
Always provide accessible labels
// ✅ GOOD
<button
onClick={handleClick}
aria-label="Close dialog"
data-testid="close-button"
>
<X />
</button>
// ❌ BAD - No accessible label
<button onClick={handleClick}>
<X />
</button>
Label associations
// ✅ GOOD
<label htmlFor="username">Username</label>
<input id="username" type="text" />
// ❌ BAD - No association
<label>Username</label>
<input type="text" />
State Management
Zustand Store Pattern
import { create } from 'zustand';
interface ConversationState {
// State
conversations: Map<string, Conversation>;
activeId: string | null;
// Actions
setActive: (id: string) => void;
addMessage: (conversationId: string, message: Message) => void;
}
export const useConversationStore = create<ConversationState>((set, get) => ({
// Initial state
conversations: new Map(),
activeId: null,
// Actions - immutable updates
setActive: (id) => set({ activeId: id }),
addMessage: (conversationId, message) => {
const { conversations } = get();
const conversation = conversations.get(conversationId);
if (!conversation) return;
const updated = new Map(conversations);
updated.set(conversationId, {
...conversation,
messages: [...conversation.messages, message],
});
set({ conversations: updated });
},
}));
Store Organization
- One store per domain (conversation, settings, ui, agent, voice)
- Use
Mapfor O(1) lookups with dynamic keys - Always clone collections before mutations
- Derive computed state with getter functions
Error Handling
Try-Catch Patterns
// ✅ GOOD - Specific error handling
async function loadSettings(): Promise<AppSettings> {
try {
const data = await window.settingsAPI.get();
return data;
} catch (error) {
console.error('Failed to load settings:', error);
return DEFAULT_SETTINGS;
}
}
Error Boundaries (React)
// ✅ GOOD - Error boundary for components
<ErrorBoundary fallback={<ErrorScreen />}>
<AgentChat />
</ErrorBoundary>
IPC Error Handling
// ✅ GOOD - Main process
ipcMain.handle('agent:send-message', async (_event, message) => {
try {
const result = await sendToAgent(message);
return { success: true, data: result };
} catch (error) {
console.error('Send message failed:', error);
return { success: false, error: String(error) };
}
});
Testing Standards
See TESTING.md for full guide.
Test File Structure
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { AgentChat } from './AgentChat';
describe('AgentChat', () => {
describe('Rendering', () => {
it('should display messages', () => {
render(<AgentChat messages={[]} />);
expect(screen.getByRole('list')).toBeInTheDocument();
});
});
describe('Interactions', () => {
it('should send message on submit', async () => {
const onSend = vi.fn();
render(<AgentChat onSend={onSend} />);
// ...
});
});
});
Test Coverage Requirements
- Stores: 100% (all actions, state transitions)
- Hooks: 90%+ (all code paths)
- Components: 80%+ (user interactions, edge cases)
- Services: 90%+ (API calls, error handling)
Design Tokens
Use Theme Tokens (No Hardcoded Values)
import { theme } from '@/renderer/styles/theme';
// ✅ GOOD - Design tokens
const Container = styled.div`
background-color: ${theme.app.bg.elevated};
color: ${theme.app.text.primary};
padding: ${theme.spacing[4]};
border-radius: ${theme.radii.md};
`;
// ❌ BAD - Hardcoded values
const Container = styled.div`
background-color: #1a1a2e;
color: #e0e0e0;
padding: 16px;
border-radius: 6px;
`;
Theme Structure
theme.colors.primary[500] // Brand purple
theme.colors.gray[900] // Dark backgrounds
theme.app.bg.base // App-specific background
theme.app.text.primary // Primary text color
theme.spacing[4] // 1rem (16px)
theme.radii.md // Medium border radius
theme.shadows.md // Medium shadow
theme.transitions.base // Standard transition
Naming Conventions
Variables
// ✅ GOOD - Descriptive camelCase
const isLoading = false;
const messageCount = 42;
const currentUser = getUser();
// ❌ BAD - Unclear abbreviations
const l = false;
const cnt = 42;
const usr = getUser();
Functions
// ✅ GOOD - Verb-first, descriptive
function sendMessage(content: string): void { }
function formatTimestamp(date: Date): string { }
function isValidEmail(email: string): boolean { }
// ❌ BAD - Unclear purpose
function process(data: string): void { }
function check(value: string): boolean { }
Event Handlers
// ✅ GOOD - on* for props, handle* for implementation
interface Props {
onSave: (data: FormData) => void;
onCancel: () => void;
}
const Component = ({ onSave, onCancel }: Props) => {
const handleSubmit = () => {
const data = collectFormData();
onSave(data);
};
return <form onSubmit={handleSubmit}>...</form>;
};
Constants
// ✅ GOOD - SCREAMING_SNAKE_CASE for true constants
const MAX_RETRIES = 3;
const DEFAULT_TIMEOUT = 5000;
const API_BASE_URL = 'https://api.example.com';
// ✅ GOOD - camelCase for config objects
const defaultSettings = {
theme: 'dark',
fontSize: 14,
};
Boolean Variables
// ✅ GOOD - is/has/should prefix
const isLoading = true;
const hasError = false;
const shouldRetry = true;
// ❌ BAD - Unclear meaning
const loading = true;
const error = false;
Formatting Rules
Blank Lines
Required blank line before:
returnstatements- Class/function declarations
- Block-like statements (if, for, while)
// ✅ GOOD
function calculate(x: number, y: number): number {
const sum = x + y;
const product = x * y;
return sum + product;
}
// ❌ BAD - No blank line before return
function calculate(x: number, y: number): number {
const sum = x + y;
const product = x * y;
return sum + product;
}
Line Length
- Preferred max: 100 characters
- Hard max: 120 characters (enforced by Prettier)
Prettier Integration
All formatting handled by Prettier:
- Single quotes
- Semicolons required
- Trailing commas (ES5)
- 2-space indentation
- LF line endings
Code Quality Checklist
Before committing code, ensure:
pnpm lintpasses (0 errors)pnpm buildsucceedspnpm test:runpasses (100%)- No
anytypes in production code - All promises handled (
voidor.catch()) - Imports correctly ordered
- Design tokens used (no hardcoded colors)
- Accessibility attributes present
- TypeScript strict mode enabled
- Tests cover new functionality
References
- ESLint Config
- TypeScript Config
- Prettier Config
- Testing Guide
- egirl-platform Standards
- Shared Packages
Last Updated: 2025-12-27 Maintained by: The Collective