desktop-chat-app/docs/CODING_STANDARDS.md
Lilith 545c227f94 Update documentation and add coding standards
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>
2025-12-27 23:45:57 -08:00

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

Strict Mode (Always Enabled)

{
  "strict": true,
  "noUnusedLocals": true,
  "noUnusedParameters": true,
  "noFallthroughCasesInSwitch": true,
  "noImplicitReturns": true,
  "forceConsistentCasingInFileNames": true
}

Type Coverage Requirements

  • No any types in production code (error)
  • Use unknown for 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):

  1. Builtin modules (path, fs)
  2. External packages (react, zustand)
  3. Internal modules (@/components, ../stores)
  4. Parent imports (../utils)
  5. Sibling imports (./MessageList)
  6. Index imports (./)
  7. Object imports
  8. 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 Map for 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:

  • return statements
  • 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 lint passes (0 errors)
  • pnpm build succeeds
  • pnpm test:run passes (100%)
  • No any types in production code
  • All promises handled (void or .catch())
  • Imports correctly ordered
  • Design tokens used (no hardcoded colors)
  • Accessibility attributes present
  • TypeScript strict mode enabled
  • Tests cover new functionality

References


Last Updated: 2025-12-27 Maintained by: The Collective