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 any type - 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.


Back to top

OpenWork Documentation - Community documentation for accomplish-ai/openwork