Code Conventions
This document outlines the coding standards and conventions used in the Openwork project. All contributors should follow these guidelines to maintain consistency and code quality.
TypeScript Requirements
Strict TypeScript Usage
- TypeScript everywhere: No JavaScript files for application logic
- Type annotations: Always provide explicit types for function parameters, return values, and variables
- No implicit any: Avoid
anytype - use specific types or create custom types - Strict null checks: Enable strict null checks in tsconfig
Type Organization
Shared Types
// packages/shared/src/types/index.ts
export interface Task {
id: string;
description: string;
status: TaskStatus;
createdAt: Date;
completedAt?: Date;
}
export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed';
// packages/shared/src/ipc.ts
export interface IPC {
task: {
create: (description: string) => Promise<Task>;
execute: (taskId: string) => Promise<void>;
cancel: (taskId: string) => Promise<void>;
};
}
IPC API Matching
Main Process Handler (apps/desktop/src/main/ipc/handlers.ts):
// src/main/ipc/handlers.ts
import { ipcMain } from 'electron';
import { Task } from '@accomplish/shared';
export function registerTaskHandlers() {
ipcMain.handle('task:create', async (event, description: string) => {
// Implementation
});
ipcMain.handle('task:execute', async (event, taskId: string) => {
// Implementation
});
}
Preload API (apps/desktop/src/preload/index.ts):
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron';
import { IPC } from '@accomplish/shared';
contextBridge.exposeInMainWorld('accomplish', {
task: {
create: (description: string) => ipcRenderer.invoke('task:create', description),
execute: (taskId: string) => ipcRenderer.invoke('task:execute', taskId),
},
});
Renderer Usage (apps/desktop/src/renderer/):
// src/renderer/lib/accomplish.ts
import { IPC } from '@accomplish/shared';
export class AccomplishAPI {
private task: IPC['task'];
constructor() {
this.task = window.accomplish.task;
}
async createTask(description: string): Promise<Task> {
return this.task.create(description);
}
}
Architecture Conventions
Monorepo Structure
openwork-repo/
├── apps/
│ └── desktop/
│ ├── src/
│ │ ├── main/ # Electron main process
│ │ ├── preload/ # Preload script
│ │ └── renderer/ # React UI
│ ├── public/ # Static assets
│ └── package.json
├── packages/
│ └── shared/
│ ├── src/
│ │ ├── types/ # Shared TypeScript types
│ │ └── ipc.ts # IPC interface definitions
│ └── package.json
└── package.json # Root workspace configuration
Process Communication
Renderer to Main Flow:
Renderer (React)
↓ window.accomplish.* calls
Preload (contextBridge)
↓ ipcRenderer.invoke
Main Process
↓ Native APIs (keytar, node-pty, electron-store)
↑ IPC events
Preload
↑ ipcRenderer.on callbacks
Renderer
File Organization
Main Process Files
Index File (src/main/index.ts):
import { app, BrowserWindow } from 'electron';
import { registerTaskHandlers } from './ipc/handlers';
import { setupAccomplishProtocol } from './protocol';
export function main() {
// Single instance enforcement
if (!app.requestSingleInstanceLock()) {
app.quit();
return;
}
// Protocol handler
setupAccomplishProtocol();
// IPC handlers
registerTaskHandlers();
// Window management
app.whenReady().then(createWindow);
}
IPC Handlers (src/main/ipc/handlers.ts):
import { ipcMain } from 'electron';
import { TaskHistoryStore } from '../store/taskHistory';
import { SecureStorage } from '../store/secureStorage';
export function registerTaskHandlers() {
const taskHistory = new TaskHistoryStore();
const secureStorage = new SecureStorage();
ipcMain.handle('task:create', async (event, description: string) => {
// Implementation
});
}
Renderer Files
Store Pattern (src/renderer/stores/taskStore.ts):
import { create } from 'zustand';
import { Task } from '@accomplish/shared';
interface TaskState {
tasks: Task[];
currentTask: Task | null;
isExecuting: boolean;
// Actions
createTask: (description: string) => void;
executeTask: (taskId: string) => void;
updateTask: (taskId: string, updates: Partial<Task>) => void;
}
export const useTaskStore = create<TaskState>((set, get) => ({
tasks: [],
currentTask: null,
isExecuting: false,
createTask: (description: string) => {
// Implementation
},
executeTask: (taskId: string) => {
// Implementation
},
}));
Command Conventions
Using Desktop-Specific Commands
# Always use -F flag for desktop-specific commands
pnpm -F @accomplish/desktop test:e2e
pnpm -F @accomplish desktop build:desktop
Environment Variable Usage
// Environment helper
const isCleanStart = process.env.CLEAN_START === '1';
const skipAuth = process.env.E2E_SKIP_AUTH === '1';
// Usage
if (isCleanStart) {
clearStoredData();
}
if (skipAuth) {
bypassOnboarding();
}
Security Conventions
API Key Storage
Secure Storage (src/main/store/secureStorage.ts):
import * as keytar from 'keytar';
export class SecureStorage {
private readonly serviceName = 'openwork';
async storeApiKey(provider: string, apiKey: string): Promise<void> {
await keytar.setPassword(this.serviceName, provider, apiKey);
}
async getApiKey(provider: string): Promise<string | null> {
return keytar.getPassword(this.serviceName, provider);
}
}
Node.js Spawning with Bundled Binaries
Correct Spawning Pattern:
import { spawn } from 'child_process';
import { getNpxPath, getBundledNodePaths } from '../utils/bundled-node';
// Get bundled paths
const npxPath = getNpxPath();
const bundledPaths = getBundledNodePaths();
// Build environment with bundled node in PATH
let spawnEnv: NodeJS.ProcessEnv = { ...process.env };
if (bundledPaths) {
const delimiter = process.platform === 'win32' ? ';' : ':';
spawnEnv.PATH = `${bundledPaths.binDir}${delimiter}${process.env.PATH || ''}`;
}
// Spawn with modified environment
spawn(npxPath, ['-y', 'some-package@latest'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: spawnEnv,
});
Error Handling Conventions
Standard Error Pattern
// Main process
ipcMain.handle('task:create', async (event, description: string) => {
try {
if (!description?.trim()) {
throw new Error('Task description cannot be empty');
}
const task = await createTask(description);
return task;
} catch (error) {
console.error('Failed to create task:', error);
throw error; // Re-throw for renderer to handle
}
});
// Renderer
try {
const task = await accomplish.createTask(description);
// Handle success
} catch (error) {
console.error('Failed to create task:', error);
showErrorToast(error.message);
}
Type-Safe Error Handling
// Custom error types
export class TaskError extends Error {
constructor(
message: string,
public code: TaskErrorCode,
public details?: unknown
) {
super(message);
this.name = 'TaskError';
}
}
export type TaskErrorCode =
| 'INVALID_TASK'
| 'EXECUTION_FAILED'
| 'PERMISSION_DENIED'
| 'NETWORK_ERROR';
// Usage
if (error instanceof TaskError) {
switch (error.code) {
case 'PERMISSION_DENIED':
showPermissionDeniedDialog();
break;
// Handle other cases
}
}
Testing Conventions
Unit Test Structure
// src/main/__tests__/ipc/handlers.test.ts
import { registerTaskHandlers } from '../ipc/handlers';
import { mock } from 'jest-mock-extended';
describe('Task Handlers', () => {
let mockIpcMain: jest.Mocked<typeof import('electron').ipcMain>;
beforeEach(() => {
mockIpcMain = mock(require('electron'));
registerTaskHandlers();
});
test('should create task with valid description', async () => {
// Arrange
const description = 'Test task';
// Act
const result = await mockIpcMain.handle.mock.results[0].value(
{} as any,
description
);
// Assert
expect(result).toMatchObject({
id: expect.any(String),
description,
status: 'pending',
});
});
});
E2E Test Structure
// apps/desktop/playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: false, // Electron requires serial execution
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
Performance Conventions
Debouncing and Throttling
import { debounce } from 'lodash-es';
// Debounced search
const debouncedSearch = debounce(async (query: string) => {
const results = await searchTasks(query);
setSearchResults(results);
}, 300);
// Usage: <input onChange={(e) => debouncedSearch(e.target.value)} />
Memoization
import { memo } from 'react';
interface TaskComponentProps {
task: Task;
onExecute: () => void;
}
const TaskComponent = memo(({ task, onExecute }: TaskComponentProps) => {
// Component implementation
return (
<div className="task-card">
<h3>{task.description}</h3>
<button onClick={onExecute}>Execute</button>
</div>
);
});
Documentation Conventions
Code Comments
/**
* Creates a new task with the given description.
*
* @param description - The task description (must be non-empty)
* @returns Promise resolving to the created task
* @throws {TaskError} If description is invalid or task creation fails
*/
export async function createTask(description: string): Promise<Task> {
// Implementation
}
JSDoc for Exports
/**
* The main IPC interface exposed to the renderer process.
* Provides type-safe methods for all main process communications.
*/
export interface IPC {
/**
* Task-related operations
*/
task: {
/**
* Creates a new task
* @param description - Task description
* @returns Created task
*/
create: (description: string) => Promise<Task>;
/**
* Executes a task
* @param taskId - ID of task to execute
*/
execute: (taskId: string) => Promise<void>;
};
}
Conclusion
Following these conventions helps maintain code quality, consistency, and readability across the Openwork project. When in doubt, follow the existing patterns in the codebase and ask for clarification in code reviews.
Remember that conventions may evolve as the project grows - stay updated by checking the latest version of this document and reviewing existing code.