Version: 1.0 Last Updated: February 7, 2026
This guide provides comprehensive technical information for developers who want to contribute to Virtual Office, deploy it, or integrate it into their systems.
- Architecture Overview
- Development Setup
- Project Structure
- Technology Stack
- Core Components
- API Reference
- Testing
- Build & Deployment
- Contributing
- Troubleshooting
Virtual Office is a full-stack application with real-time communication between AIOS agents and the frontend UI.
┌─────────────────────────────────────────────────────────┐
│ Virtual Office UI │
│ (React 18 + Vite) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Canvas │ │ Agent │ │ Activity │ │
│ │ Renderer │ │ Status │ │ Detail │ │
│ │ (2D Iso) │ │ Grid │ │ Panel │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ State Management (Zustand) │ │
│ └─────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────┘
│ WebSocket (Socket.io Client)
│
┌────────────────────┴────────────────────────────────────┐
│ Backend API Server │
│ (Node.js + Express) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Socket.io Server │ │
│ │ (Rooms per project, Broadcasting) │ │
│ └──────────────┬──────────────────────────────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ ┌─────────────────┐ │
│ │ AIOS Connector │ │ SQLite DB │ │
│ │ (IPC/WebSocket to AIOS) │ │ (Projects, │ │
│ │ │ │ Activities) │ │
│ └──────────────┬──────────────┘ └─────────────────┘ │
└─────────────────┴─────────────────────────────────────┘
│
┌─────────┴─────────┐
│ AIOS Framework │
│ (Local Instance) │
└───────────────────┘
See Technical Architecture for detailed ADRs.
| Component | Technology | Rationale |
|---|---|---|
| Frontend | React 18 + Vite | Mature ecosystem, concurrent features for real-time |
| Rendering | Canvas 2D Isometric | MVP velocity (3-5x faster than 3D) |
| State | Zustand | Simple, performant, real-time friendly |
| WebSocket | Socket.io | Auto-reconnect, rooms, broadcasting |
| Database | SQLite (Drizzle ORM) | Zero config, portable, easy migration |
| Testing | Vitest + Playwright | Fast, Vite-native, multi-browser |
AIOS Agent Action
↓
AIOS Connector (Backend)
↓
Store in SQLite
↓
Broadcast via Socket.io
↓
Zustand Store (Frontend)
↓
React Components Re-render
↓
Canvas Updates (Visual)
- Node.js: 18.0.0 or higher
- npm: 9.0.0 or higher
- Git: Latest version
- AIOS Framework: 2.0+ (for testing integration)
- Code Editor: VS Code recommended
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next"
]
}- Clone the repository
git clone https://github.com/synkra/virtual-office.git
cd virtual-office- Install dependencies
# Install all workspace dependencies
npm installThis installs dependencies for:
- Root workspace
- Frontend package
- Backend package
- Shared types package
- Configure environment variables
# Backend configuration
cp backend/.env.example backend/.envEdit backend/.env:
# Server Configuration
PORT=3001
NODE_ENV=development
# Frontend URL (for CORS)
FRONTEND_URL=http://localhost:3000
# AIOS Connection
AIOS_WS_URL=ws://localhost:8080
# Database
DB_PATH=./data/virtual-office.db
# License (for testing Enterprise features)
LICENSE_SECRET_KEY=your-secret-key-here
# Optional: Monitoring
SENTRY_DSN=
POSTHOG_KEY=- Initialize database
cd backend
npm run db:generate # Generate Drizzle schema
npm run db:push # Apply to SQLite- Start development servers
# From root directory
npm run devThis starts:
- Frontend: http://localhost:3000 (Vite dev server with HMR)
- Backend: http://localhost:3001 (Express + Socket.io with hot reload)
- Verify setup
Open http://localhost:3000 in your browser. You should see the Virtual Office UI.
# Start both frontend + backend in watch mode
npm run dev
# Run tests (all workspaces)
npm test
# Run tests in watch mode
npm run test:watch
# Lint code
npm run lint
# Format code
npm run format
# Type checking
npm run typecheck
# Build for production
npm run buildPre-configured hooks:
- pre-commit: Runs
npm run lintto catch style issues - pre-push: Runs
npm testto prevent broken code
To skip hooks (not recommended):
git commit --no-verifyvirtual-office/
├── .github/ # GitHub Actions workflows
│ └── workflows/
│ ├── ci.yml # Continuous integration
│ └── deploy.yml # Deployment pipeline
│
├── frontend/ # React application
│ ├── public/ # Static assets
│ ├── src/
│ │ ├── components/ # React components (Atomic Design)
│ │ │ ├── ui/ # Atoms (buttons, inputs)
│ │ │ ├── AgentWorkspace.tsx
│ │ │ ├── AgentDetailPanel.tsx
│ │ │ └── OfficeView.tsx
│ │ ├── stores/ # Zustand state stores
│ │ │ ├── agentStore.ts
│ │ │ ├── projectStore.ts
│ │ │ └── uiStore.ts
│ │ ├── lib/ # Utilities and helpers
│ │ │ ├── socket.ts
│ │ │ └── utils.ts
│ │ ├── hooks/ # Custom React hooks
│ │ ├── types/ # TypeScript types
│ │ └── App.tsx
│ ├── index.html
│ ├── vite.config.ts # Vite configuration
│ ├── tailwind.config.ts
│ └── package.json
│
├── backend/ # Node.js server
│ ├── src/
│ │ ├── routes/ # Express routes
│ │ │ ├── projects.ts
│ │ │ └── activities.ts
│ │ ├── services/ # Business logic
│ │ │ ├── aiosConnector.ts
│ │ │ ├── activityService.ts
│ │ │ └── projectService.ts
│ │ ├── db/ # Database layer
│ │ │ ├── schema.ts # Drizzle schema
│ │ │ ├── migrations/
│ │ │ └── index.ts
│ │ ├── websocket/ # Socket.io handlers
│ │ │ └── handlers.ts
│ │ └── index.ts # Server entry point
│ ├── .env.example
│ ├── drizzle.config.ts
│ └── package.json
│
├── shared/ # Shared TypeScript types
│ ├── src/
│ │ └── types/
│ │ ├── agent.ts
│ │ ├── project.ts
│ │ └── activity.ts
│ └── package.json
│
├── docs/ # Documentation
│ ├── architecture/ # Architecture documents and ADRs
│ ├── stories/ # User stories
│ ├── prd.md # Product requirements
│ ├── USER_GUIDE.md
│ └── DEVELOPER_GUIDE.md
│
├── package.json # Root package (workspace manager)
├── tsconfig.json # TypeScript configuration
└── README.md
frontend/src/components/
├── ui/ # Atoms - Basic building blocks
│ ├── button.tsx
│ ├── input.tsx
│ ├── dialog.tsx
│ └── dropdown-menu.tsx
│
├── AgentStatus.tsx # Molecules - Simple components
├── AgentWorkspace.tsx
├── ActivityItem.tsx
│
├── AgentDetailPanel.tsx # Organisms - Complex components
├── AgentGrid.tsx
├── ProjectSwitcher.tsx
│
├── OfficeView.tsx # Templates - Page layouts
└── SettingsView.tsx
| Package | Version | Purpose |
|---|---|---|
| React | 18.2.0 | UI framework |
| TypeScript | 5.3.3 | Type safety |
| Vite | 5.0.12 | Build tool & dev server |
| Zustand | 4.5.7 | State management |
| React Query | 5.17.19 | Server state & caching |
| Socket.io Client | 4.8.3 | WebSocket client |
| Tailwind CSS | 3.4.1 | Styling |
| Radix UI | Various | Accessible components |
| Lucide React | 0.314.0 | Icons |
| Package | Version | Purpose |
|---|---|---|
| Node.js | 18+ | Runtime |
| Express | 4.18.2 | HTTP server |
| Socket.io | 4.6.1 | WebSocket server |
| SQLite | via better-sqlite3 | Database |
| Drizzle ORM | 0.29.3 | Type-safe ORM |
| TypeScript | 5.3.3 | Type safety |
| dotenv | 16.4.0 | Environment config |
| Package | Version | Purpose |
|---|---|---|
| Vitest | 1.2.1 | Unit & integration tests |
| Playwright | Latest | E2E tests |
| Testing Library | 14.1.2 | React component tests |
| jsdom | 24.0.0 | DOM simulation |
- ESLint: Code linting
- Prettier: Code formatting
- Husky: Git hooks
- tsx: TypeScript execution (backend)
- Drizzle Kit: Database migrations
Purpose: Root component that renders the virtual office
Location: frontend/src/components/OfficeView.tsx
Key Features:
- Renders agent grid
- Manages WebSocket connection
- Handles project switching
- Opens/closes detail panel
Props:
interface OfficeViewProps {
projectId: string;
}State:
const agents = useAgentStore((state) => state.agents);
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);Purpose: Visual representation of a single agent
Location: frontend/src/components/AgentWorkspace.tsx
Props:
interface AgentWorkspaceProps {
agent: Agent;
onClick: (agentId: string) => void;
}Visual States:
- Online: Green glow, seated agent, active monitor
- Offline: Gray appearance, empty chair, inactive monitor
Purpose: Sidebar panel showing detailed activity log
Location: frontend/src/components/AgentDetailPanel.tsx
Props:
interface AgentDetailPanelProps {
agentId: string;
isOpen: boolean;
onClose: () => void;
}Features:
- Infinite scroll for activity history
- Real-time activity updates
- Copy to clipboard
- Export to JSON/CSV
- Filtering and search
Implementation Details:
// Fetch activities
const { data: activities } = useQuery({
queryKey: ['activities', agentId],
queryFn: () => fetchActivities(agentId)
});
// Real-time updates
useEffect(() => {
socket.on('agent_update', (update) => {
if (update.agentId === agentId) {
// Update activity list
}
});
}, [agentId]);Purpose: Bridge between AIOS and Virtual Office
Location: backend/src/services/aiosConnector.ts
Key Methods:
class AIOSConnector {
private ws: WebSocket | null = null;
// Connect to AIOS instance
async connect(url: string): Promise<void> {
this.ws = new WebSocket(url);
this.ws.on('message', this.handleMessage.bind(this));
this.ws.on('error', this.handleError.bind(this));
}
// Handle incoming AIOS events
private handleMessage(data: Buffer): void {
const event: AIOSEvent = JSON.parse(data.toString());
this.emit('aios_event', event);
}
// Disconnect from AIOS
disconnect(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}Event Types:
interface AIOSEvent {
type: 'agent_activated' | 'agent_deactivated' | 'task_started'
| 'task_completed' | 'activity';
projectId: string;
agentId: string;
timestamp: string;
payload?: {
activityType?: 'code' | 'command' | 'file_op' | 'tool_call';
content?: string;
filePath?: string;
};
}Purpose: Manage activity log storage and retrieval
Location: backend/src/services/activityService.ts
Key Methods:
class ActivityService {
// Store new activity
async createActivity(activity: NewActivity): Promise<Activity> {
const [inserted] = await db
.insert(activities)
.values(activity)
.returning();
return inserted;
}
// Get activities for agent
async getActivitiesByAgent(
agentId: string,
limit = 50
): Promise<Activity[]> {
return db
.select()
.from(activities)
.where(eq(activities.agentId, agentId))
.orderBy(desc(activities.timestamp))
.limit(limit);
}
// Filter activities
async filterActivities(filters: ActivityFilters): Promise<Activity[]> {
let query = db.select().from(activities);
if (filters.type) {
query = query.where(eq(activities.type, filters.type));
}
if (filters.startDate) {
query = query.where(gte(activities.timestamp, filters.startDate));
}
return query.orderBy(desc(activities.timestamp)).limit(filters.limit || 50);
}
// Cleanup old activities
async cleanupOldActivities(daysToKeep = 30): Promise<number> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
const result = await db
.delete(activities)
.where(lt(activities.timestamp, cutoffDate));
return result.rowsAffected;
}
}Purpose: Handle Socket.io connections and broadcasts
Location: backend/src/websocket/handlers.ts
Implementation:
import { Server as SocketIOServer } from 'socket.io';
export function setupWebSocketHandlers(io: SocketIOServer) {
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
// Subscribe to project
socket.on('subscribe_project', async ({ projectId }) => {
socket.join(`project:${projectId}`);
// Send initial state
const agents = await getProjectAgents(projectId);
const activities = await getRecentActivities(projectId);
socket.emit('initial_state', { agents, activities });
});
// Unsubscribe from project
socket.on('unsubscribe_project', ({ projectId }) => {
socket.leave(`project:${projectId}`);
});
// Handle disconnection
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
// Broadcast agent updates
aiosConnector.on('aios_event', async (event: AIOSEvent) => {
// Store in database
await activityService.createActivity({
projectId: event.projectId,
agentId: event.agentId,
type: event.payload?.activityType || 'unknown',
content: event.payload?.content,
filePath: event.payload?.filePath,
timestamp: new Date(event.timestamp)
});
// Broadcast to clients
io.to(`project:${event.projectId}`).emit('agent_update', event);
});
}Location: frontend/src/stores/agentStore.ts
interface AgentState {
agents: Record<string, Agent>;
updateAgent: (id: string, update: Partial<Agent>) => void;
setAgents: (agents: Agent[]) => void;
getAgent: (id: string) => Agent | undefined;
}
export const useAgentStore = create<AgentState>((set, get) => ({
agents: {},
updateAgent: (id, update) =>
set((state) => ({
agents: {
...state.agents,
[id]: { ...state.agents[id], ...update }
}
})),
setAgents: (agents) =>
set({
agents: Object.fromEntries(agents.map(a => [a.id, a]))
}),
getAgent: (id) => get().agents[id]
}));Location: frontend/src/stores/projectStore.ts
interface ProjectState {
projects: Project[];
activeProjectId: string | null;
setProjects: (projects: Project[]) => void;
setActiveProject: (id: string) => void;
addProject: (project: Project) => void;
removeProject: (id: string) => void;
}
export const useProjectStore = create<ProjectState>()(
persist(
(set) => ({
projects: [],
activeProjectId: null,
setProjects: (projects) => set({ projects }),
setActiveProject: (id) => set({ activeProjectId: id }),
addProject: (project) =>
set((state) => ({
projects: [...state.projects, project]
})),
removeProject: (id) =>
set((state) => ({
projects: state.projects.filter(p => p.id !== id),
activeProjectId: state.activeProjectId === id
? null
: state.activeProjectId
}))
}),
{
name: 'virtual-office-projects' // localStorage key
}
)
);Base URL: http://localhost:3001
GET /api/projects
// Get all projects
Response: {
projects: Project[]
}POST /api/projects
// Add new project
Request: {
name: string;
path: string;
}
Response: {
project: Project
}DELETE /api/projects/:id
// Remove project
Response: {
success: boolean
}GET /api/activities/:projectId
// Get activities for project
Query params:
- agentId?: string
- type?: 'code' | 'command' | 'file_op' | 'tool_call'
- limit?: number (default: 50)
- offset?: number
Response: {
activities: Activity[];
total: number;
}GET /api/activities/agent/:agentId
// Get activities for specific agent
Response: {
activities: Activity[]
}subscribe_project
socket.emit('subscribe_project', {
projectId: string
});unsubscribe_project
socket.emit('unsubscribe_project', {
projectId: string
});initial_state
socket.on('initial_state', (data: {
agents: Agent[];
activities: Activity[];
}) => {
// Handle initial state
});agent_update
socket.on('agent_update', (event: AIOSEvent) => {
// Handle agent status or activity update
});connection_status
socket.on('connection_status', (status: {
aiosConnected: boolean;
message?: string;
}) => {
// Handle AIOS connection status
});# All tests
npm test
# Watch mode
npm run test:watch
# Coverage report
npm run test:coverage
# Frontend only
npm test --workspace=frontend
# Backend only
npm test --workspace=backendfrontend/src/
├── components/
│ ├── AgentWorkspace.tsx
│ └── AgentWorkspace.test.tsx
├── stores/
│ ├── agentStore.ts
│ └── agentStore.test.ts
└── lib/
├── utils.ts
└── utils.test.ts
backend/src/
├── services/
│ ├── activityService.ts
│ └── activityService.test.ts
└── websocket/
├── handlers.ts
└── handlers.test.ts
Frontend Component Test:
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { AgentWorkspace } from './AgentWorkspace';
describe('AgentWorkspace', () => {
it('renders agent name', () => {
const agent = {
id: 'dev-1',
name: 'Dev Agent',
role: 'dev',
status: 'online'
};
render(<AgentWorkspace agent={agent} onClick={() => {}} />);
expect(screen.getByText('Dev Agent')).toBeInTheDocument();
});
it('shows online status', () => {
const agent = {
id: 'dev-1',
name: 'Dev Agent',
role: 'dev',
status: 'online'
};
render(<AgentWorkspace agent={agent} onClick={() => {}} />);
const workspace = screen.getByRole('button');
expect(workspace).toHaveClass('online');
});
});Backend Service Test:
import { describe, it, expect, beforeEach } from 'vitest';
import { ActivityService } from './activityService';
describe('ActivityService', () => {
let service: ActivityService;
beforeEach(() => {
// Use in-memory database for tests
service = new ActivityService(':memory:');
});
it('creates activity', async () => {
const activity = await service.createActivity({
projectId: 'proj-1',
agentId: 'dev-1',
type: 'code',
content: 'console.log("test")',
timestamp: new Date()
});
expect(activity.id).toBeDefined();
expect(activity.type).toBe('code');
});
it('filters activities by type', async () => {
// Create test activities
await service.createActivity({
projectId: 'proj-1',
agentId: 'dev-1',
type: 'code',
content: 'test code',
timestamp: new Date()
});
await service.createActivity({
projectId: 'proj-1',
agentId: 'dev-1',
type: 'command',
content: 'npm test',
timestamp: new Date()
});
// Filter
const codeActivities = await service.filterActivities({
type: 'code'
});
expect(codeActivities).toHaveLength(1);
expect(codeActivities[0].type).toBe('code');
});
});Location: tests/e2e/
import { test, expect } from '@playwright/test';
test.describe('Virtual Office', () => {
test('connects to project and monitors agents', async ({ page }) => {
// Navigate to app
await page.goto('http://localhost:3000');
// Add project
await page.click('[data-testid="add-project-button"]');
await page.fill('[data-testid="project-path"]', '/path/to/test-project');
await page.click('[data-testid="connect-button"]');
// Verify connection
await expect(page.locator('[data-testid="status-indicator"]'))
.toHaveText('Connected');
// Verify agent grid visible
await expect(page.locator('[data-testid="agent-grid"]')).toBeVisible();
// Check agent appears online
const devAgent = page.locator('[data-agent="dev"]');
await expect(devAgent).toHaveClass(/online/);
// Click agent to open detail panel
await devAgent.click();
// Verify detail panel opens
await expect(page.locator('[data-testid="agent-detail-panel"]'))
.toBeVisible();
// Verify activity log visible
await expect(page.locator('[data-testid="activity-log"]'))
.toBeVisible();
});
test('filters activity by type', async ({ page }) => {
await page.goto('http://localhost:3000');
// Open agent detail panel
await page.click('[data-agent="dev"]');
// Select filter type
await page.click('[data-testid="type-filter"]');
await page.click('[data-testid="filter-code"]');
// Verify only code activities shown
const activities = await page.locator('[data-testid="activity-item"]').all();
for (const activity of activities) {
await expect(activity.locator('[data-testid="activity-type"]'))
.toHaveText('code');
}
});
});| Area | Target Coverage |
|---|---|
| Backend Services | 80%+ |
| Frontend Components | 60%+ |
| State Stores | 90%+ |
| Utilities | 95%+ |
# Build all packages
npm run build
# Output:
# frontend/dist/ - Static files for deployment
# backend/dist/ - Compiled JavaScriptBackend (.env.production):
NODE_ENV=production
PORT=3001
FRONTEND_URL=https://virtual-office.synkra.com
AIOS_WS_URL=wss://aios-server.synkra.com
DB_PATH=/var/data/virtual-office.db
LICENSE_SECRET_KEY=<production-secret>
SENTRY_DSN=<sentry-dsn>Frontend (build-time):
VITE_API_URL=https://api.virtual-office.synkra.com
VITE_WS_URL=wss://api.virtual-office.synkra.com
VITE_SENTRY_DSN=<sentry-dsn>
VITE_POSTHOG_KEY=<posthog-key>Frontend: Deploy to Vercel/Netlify
# Install Vercel CLI
npm i -g vercel
# Deploy frontend
cd frontend
vercel --prodBackend: Deploy to Railway/Fly.io
# Install Railway CLI
npm i -g @railway/cli
# Deploy backend
cd backend
railway upDockerfile (backend):
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
COPY backend/package*.json ./backend/
RUN npm ci --workspace=backend
COPY backend/ ./backend/
COPY shared/ ./shared/
RUN npm run build --workspace=backend
EXPOSE 3001
CMD ["node", "backend/dist/index.js"]docker-compose.yml:
version: '3.8'
services:
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
ports:
- "3000:80"
environment:
- VITE_API_URL=http://backend:3001
backend:
build:
context: .
dockerfile: backend/Dockerfile
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- DB_PATH=/data/virtual-office.db
volumes:
- vo-data:/data
volumes:
vo-data:Run with:
docker-compose up -dRequirements:
- Linux server (Ubuntu 22.04 recommended)
- Node.js 18+
- Nginx (for reverse proxy)
- PM2 (for process management)
Setup:
# Install PM2
npm install -g pm2
# Clone and build
git clone https://github.com/synkra/virtual-office.git
cd virtual-office
npm install
npm run build
# Start backend with PM2
cd backend
pm2 start npm --name "virtual-office-backend" -- start
# Serve frontend with Nginx
sudo cp -r frontend/dist/* /var/www/virtual-office/Nginx Configuration:
server {
listen 80;
server_name virtual-office.example.com;
location / {
root /var/www/virtual-office;
try_files $uri /index.html;
}
location /api {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
}
location /socket.io {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
}GitHub Actions (.github/workflows/ci.yml):
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Unit tests
run: npm test
- name: E2E tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: |
frontend/dist/
backend/dist/- Fork the repository
- Clone your fork
- Create a feature branch:
git checkout -b feature/your-feature - Make your changes
- Test thoroughly:
npm test && npm run lint - Commit:
git commit -m "feat: add your feature" - Push:
git push origin feature/your-feature - Open a Pull Request
We follow Conventional Commits:
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat: New featurefix: Bug fixdocs: Documentation onlystyle: Code style (formatting, no logic change)refactor: Code refactoringperf: Performance improvementtest: Adding or updating testschore: Maintenance tasks
Examples:
feat(frontend): add activity export to CSV
Implements CSV export functionality for agent activity logs.
Users can now export filtered activities for external analysis.
Closes #123
- TypeScript: Use strict mode, avoid
any - React: Functional components with hooks
- Naming:
- Components: PascalCase (
AgentWorkspace.tsx) - Functions: camelCase (
getAgentStatus()) - Constants: UPPER_SNAKE_CASE (
MAX_ACTIVITIES)
- Components: PascalCase (
- Formatting: Prettier (runs on pre-commit)
- Imports: Group by external → internal → relative
- Tests added/updated and passing
- Linting passes (
npm run lint) - Type checking passes (
npm run typecheck) - Documentation updated (if needed)
- Commit messages follow convention
- PR description explains changes
- No merge conflicts
- Automated checks run (CI pipeline)
- Code review by maintainers
- Requested changes addressed
- Approval from 1+ maintainers
- Merge to main branch
Error: Cannot find module '@virtual-office/shared'
Solution:
# Rebuild workspace links
npm install
# Verify workspaces
npm list --workspacesError: Type error: Cannot find type definitions
Solution:
# Regenerate tsconfig paths
npm run typecheckError: Database locked
Solution:
# Close all connections and restart backend
pm2 restart virtual-office-backend
# Or reset database (development only)
rm backend/data/virtual-office.db
npm run db:push --workspace=backendError: Migration failed
Solution:
# Generate new migration
npm run db:generate --workspace=backend
# Apply manually
npm run db:push --workspace=backendError: WebSocket connection failed
Solution:
- Check backend is running:
curl http://localhost:3001/health - Verify Socket.io endpoint:
curl http://localhost:3001/socket.io/ - Check CORS settings in
backend/src/index.ts - Inspect browser console for errors
Error: AIOS connector timeout
Solution:
- Verify AIOS is running:
ps aux | grep aios - Check AIOS WebSocket URL in
.env - Test connection manually:
wscat -c ws://localhost:8080
Problem: High CPU usage during development
Solution:
# Disable hot reload for backend
cd backend
NODE_ENV=development node dist/index.js
# Or adjust Vite HMR
# In vite.config.ts:
server: {
hmr: {
overlay: false
}
}Problem: Memory leak in long-running tests
Solution:
// Add cleanup in tests
afterEach(() => {
// Close WebSocket connections
socket?.disconnect();
// Clear stores
useAgentStore.getState().setAgents([]);
});- React Documentation
- Zustand Documentation
- Socket.io Documentation
- Drizzle ORM Documentation
- Vite Documentation
- Issues: https://github.com/synkra/virtual-office/issues
- Discussions: https://github.com/synkra/virtual-office/discussions
- Discord: Join our community
- Email: dev@synkra.com
Virtual Office Developer Guide v1.0 © 2026 Synkra AIOS Team. Licensed under MIT License.